このエントリはJava Advent Calendar 2012の20日目のエントリです。
昨日は@akirakoyasuさんのアノテーションのインスタンスを取得するでした。
明日は@Fantom_JACさんです。
パターンマッチとは
多くの関数型言語には、パターンマッチという仕組みが用意されています。
パターンマッチは、データ構造を型や値のパターンで分解する仕組みです。
例えばScalaで
val v = List(1, "hoge", 3) val List(n:Int, s:String, a) = v
などとすると、リストが分解されてそれぞれの値がn、s、aという変数に入ります。
このとき、次のように2番目の値がDoubleにマッチさせようとすると、Stringにはマッチしないので例外が発生します。
val List(n:Int, s:Double, a) = v
次のようにして値をマッチさせることもできます。また、何にでもマッチするワイルドカードも使えます。
val List(n:Int, _, 3) = v
パターンマッチは、組み合わせて条件分岐に使うと強力です。次のようにすると、リストがInt,Stringの順で始まっている2要素以上のリストであれば、2番目の文字列を表示、それ以外だと?を表示となります。
list match{ case (n:Int) :: (s:String) :: c => println(s) case _ => println("?") }
パターンマッチを使ってfizzbuzzを書くとこんな感じになりますね。
for(i <- 1 to 20){ (i % 3, i % 5) match{ case (0, 0) => println("fizzbuzz") case (0, _) => println("fizz") case (_, 0) => println("buzz") case _ => println(i) } }
switch〜case
パターンマッチでの分岐を見ると、Javaのswitch〜caseのようにも見えます。
実際、関数型言語の多くの入門書で、パターンマッチを説明するときに最初に値だけでの分岐を示しておいて「これはCやJavaのswitch〜caseのように見えますね。でもxx言語のパターンマッチはもっと強力なのです!!」という風な流れで説明されているものを見かけます。
今回の流れは「でもJava言語のパターンマッチはもっと貧弱なのです。。。」という流れなんですけど。
ともかく、switchを使うとこんな感じになりますね。
for(int i = 0; i < 20; ++i){ switch(i % 3){ case 0: System.out.println("fizz"); break; default: System.out.println(i); } }
switchのパターンマッチのいいところは、defaultがあって取りこぼしを防げるところです。
ただ、あくまで値でのマッチングで、型によって処理を振り分けるということはできません。もちろん型の名前をとってきてその名前で処理を振り分けるということはできますけど。
そして、使えるのが整数かenumか文字列の単純な値だけで、構造を分解するということもできません。
メソッドシグネチャでのパターンマッチ
Javaでは同じ名前でパラメータの型が違うメソッドは別ものとして扱われます。そこで、このことを利用して、型によって処理を振り分けるということが出来そうです。
こんな感じ。
static class Zero{} static void fz(Zero z, Object o){ System.out.println("fizz"); } static void fz(Object o, Zero z){ System.out.println("buzz"); } static void fz(Zero o1, Zero o2){ System.out.println("fizzbuzz"); } static void fz(Object o1, Object o2){ System.out.println("num"); } public static void main(String... args){ fz(new Zero(), 2); fz(1, new Zero()); fz(new Zero(), new Zero()); fz(2, 3); } }
この引数の型を使った処理の振り分けを利用したパターンにvisitパターンがあって、言語のパース処理でよく使われています。逆に、visitパターンを使うような処理をパターンマッチを使って書くと、すっきり書けます。
型が使えることと複数の値が扱えることがいいところなんですけど、データ構造の分解自体は呼び出し側で行っておく必要があります。
最大の欠点は、静的な処理振り分けなので、次のようにFizzBuzzを組んでも意図した通りには動かないということです。
次のようなコードを書いたとして、変数o3やo5にZeroオブジェクトが割り当てられていたとしても、結局コンパイル時にObject型をふたつ受け取る関数として解決されてしまっているので、numが表示されてしまいます。
for(int i = 0; i < 20; ++i){ Object o3 = i % 3 == 0 ? new Zero() : (Object)i; Object o5 = i % 5 == 0 ? new Zero() : (Object)i; fz(o3, o5); }
例外
型によるパターンマッチを動的に行えるのが、例外の仕組みです。
とりあえず、例外を使ってパターンマッチするとこんな感じですね。
public class FizzBuzz { static class Mod3 extends RuntimeException{ int num; } static class Mod3_0 extends Mod3{} static class Mod3_1 extends Mod3{} static class Mod3_2 extends Mod3{} public static void main(String... arg) throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException{ for(int i = 0; i < 20; ++i){ try{ Mod3 m3 = (Mod3)FizzBuzz.class.getClassLoader() .loadClass("advent2012.FizzBuzz$Mod3_" + i % 3).newInstance(); m3.num = i; throw m3; }catch(Mod3_0 e){ System.out.println("fizz"); }catch(Mod3 e){ System.out.println(e.num); } } } }
ただし、ひとつの値に対してしかマッチできないことと、Throwableを継承した型以外は使えないということが欠点です。また、値自体ではマッチできないということも欠点のひとつです。
せめてinterfaceでマッチできたりすると、もう少し使いやすいのだけど。
まとめ
ということで、Javaでパターンマッチとして使えそうなものには、一長三短くらいがあることがわかりました。
もっといいものがあったり、もう少し工夫できるよというのを知ってる人は、教えてください。