きしだのはてな このページをアンテナに追加 RSSフィード

2012-12-21(金) Javaでのパターンマッチを考える

[]Javaでのパターンマッチを考える 13:59 Javaでのパターンマッチを考えるを含むブックマーク

このエントリは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)
      }
    }

Javaでのパターンマッチは?

残念ながら、Javaには同じようなパターンマッチはありません。

でも、がんばってパターンマッチに使えるようなものを探してみます。


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でパターンマッチとして使えそうなものには、一長三短くらいがあることがわかりました。

もっといいものがあったり、もう少し工夫できるよというのを知ってる人は、教えてください。


Javaでパターンマッチがやりたくなったときは

例えばScalaを使う。

sempreffsempreff 2012/12/21 14:30 面白かったので、Pattern を使って書いてみました。

Pattern fizz = Pattern.compile("0.");
Pattern buzz = Pattern.compile(".0");
Pattern num = Pattern.compile("[^0][^0]");
for (int i = 0; i < 20; ++i) {
if (num.matcher((i % 3) + "" + (i % 5)).matches()) {
System.out.print(i);
} else {
if (fizz.matcher((i % 3) + "" + (i % 5)).matches()) { System.out.print("fizz"); }
if (buzz.matcher((i % 3) + "" + (i % 5)).matches()) { System.out.print("buzz"); }
}
System.out.println("");
}

あまりすっきりとは書けませんでした(苦笑

nowokaynowokay 2012/12/21 14:49 おぉ。そういえば正規表現を使うという手もありますね。

akirakoyasuakirakoyasu 2012/12/22 06:07 私はLambdaを使って書いてみました。

Re: Javaでのパターンマッチを考える
http://www.akirakoyasu.net/2012/12/22/re-thinking-pattern-matching-in-java/

nowokaynowokay 2012/12/25 04:10 おぉ、パターンマッチっぽいw