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

2013-12-12(木) Java SE 8でパターンマッチを実装する

[][]Java SE 8でパターンマッチを実装する 12:48 Java SE 8でパターンマッチを実装するを含むブックマーク

Java Advent Calendar 2013の12日目のエントリです。

昨日はtorutkさんでした。

Java Advent Calendar 2013 11日目 - Java SE 8の新クラス・メソッド一覧 - torutkの日記

明日はbitterfoxさんがマニアックな記事を書くんだと思います。


ところでパターンマッチ

実は去年のAdvent Calendarでもパターンマッチを考えていました。

Javaでのパターンマッチを考える - きしだのはてな


このときは、Javaの言語機能としてパターンマッチに使えるものはないかと考えました。

今回はJava 8ラムダを利用して、どれだけ型安全に条件分岐しつつ構造を分解できるかということにチャレンジしてみます。b115で確認してます。

値の比較に関しては、型の扱いさえできれば、まあ実装すりゃ実装できるっていう感じなので、今回は省きます。エントリを長くするだけなので。あと、戻り値が必要な場合は別の実装を用意する必要があるのですけど、それも今回は省きます。エントリを長くするだけなので。


オブジェクトの型でのパターンマッチ

まずは基本になる、単一オブジェクトの型でのパターンマッチです。実装の過程を見るのがめんどうな人は、最後の利用例だけ見ればいいと思います。

最初にこんなインタフェースを用意します。

    @FunctionalInterface
    public interface MatchCase{
        boolean match(Object o);
    }

で、それを実装したクラスを用意します。

    @AllArgsConstructor
    public static class CaseOf<T> implements MatchCase{
        Class<T> cls;
        Consumer<T> proc;
        
        @Override
        public boolean match(Object o){
            if(!cls.isInstance(o)) return false;
            proc.accept((T)o);
            return true;
        }
    }    

lombok使ってます。


それから、このクラスのオブジェクトを生成するメソッドを定義します。

    public static <T> CaseOf<T> caseOf(Class<T> cls, Consumer<T> proc){
        return new CaseOf<>(cls, proc);
    }

最後に、パターンマッチを行うメソッドを定義します。

    public static void match(Object o, MatchCase... cos){
        for(MatchCase mc: mcs){
            if(mc.match(o)) return;
        }
    }

そうすると、次のように型によってマッチさせつつ、その型の値だけを取り出すというパターンマッチが記述できます。

    Stream.of("やあ", 123, true).forEach(item -> {
        match(item,
            caseOf(Boolean.class, b -> {
                System.out.println(b ? "真" : "偽");
            }),
            caseOf(Integer.class, i -> {
                System.out.println(i < 10 ? "小さい" : "大きい");
            }),
            caseOf(String.class, s -> {
                System.out.println("そのまま:" + s);
            })
        );
    });

あとは、常にマッチするdefault的なものを用意するといいと思います。

スクリプト言語の実装するとか、言語処理系では、これだけでも案外便利です。


ところで、hoge(foo, bar -> {some();}) をhoge(foo,bar){some();}と書けるようなシンタックスシュガー欲しいですね。

そうすると上のサンプルは次のようになってもっと制御構造っぽくなります。

    Stream.of("やあ", 123, true).forEach(item) {
        match(item,
            caseOf(Boolean.class, b) {
                System.out.println(b ? "真" : "偽");
            },
            caseOf(Integer.class, i) {
                System.out.println(i < 10 ? "小さい" : "大きい");
            },
            caseOf(String.class, s) {
                System.out.println("そのまま:" + s);
            }
        );
    };

リストのパターンマッチ

構造を分解するパターンマッチとして、リストのパターンマッチを実装してみます。

まずはインタフェースを用意します。

    @FunctionalInterface
    public interface ListMatchCase<T>{
        boolean match(List<T> o);
    }

そしたら、それを実装して、空のリストにマッチするクラスを用意します。

    @AllArgsConstructor
    public static class ListEmptyCase<T> implements ListMatchCase<T>{
        private Runnable r;
        @Override
        public boolean match(List<T> list) {
            if(!list.isEmpty()) return false;
            r.run();
            return true;
        }
    }

もうひとつ、空ではないリストにマッチして、最初の要素と残りの要素にわけるクラスを用意します。

    @AllArgsConstructor
    public static class ListCase<T> implements ListMatchCase<T>{
        private BiConsumer<T, List<T>> cons;
        @Override
        public boolean match(List<T> list) {
            if(list.isEmpty()) return false;
            cons.accept(list.get(0), list.subList(1, list.size()));
            return true;
        }
    }

それらのクラスのオブジェクトを返すメソッドを用意します。

    public static <T> ListMatchCase<T> listCase(Runnable r){
        return new ListEmptyCase<>(r);
    }
    
    public static <T> ListMatchCase<T> listCase(BiConsumer<T, List<T>> bicons){
        return new ListCase<>(bicons);
    }

そして、パターンマッチのメソッドを用意します。

    public static <T> void match(List<T> list, ListMatchCase<T>... matchers){
        for(ListMatchCase<T> matcher : matchers){
            if(matcher.match(list)) return;
        }
    }

これで完成。

こんな感じで再帰によるリスト処理が書けます。関数型言語っぽいですね。

    public static void viewList(List<String> strs){
        match(strs, 
            listCase(() -> {
                System.out.println("おわり");
            }),
            listCase((s, r) -> {
                System.out.println(s);
                viewList(r);
            })
        );
    }

あとは、ひとつの要素のときにマッチするクラスを用意するとなにかと便利かもです。リストの要素がひとつであることを期待して、その要素を処理するっていうことが、結構あったりします。だいたいは何か制約からの逃げなんですけど。


ケースクラスを実装するよ

さてさて、ケースクラスを実装してみます。

まず、ケースクラスをあらわすインタフェースを用意します。

    public interface CaseClass2<T1, T2>{
    }

そしたら、こんな感じのマッチクラスを用意します。リフレクションで要素取得してます。

    @AllArgsConstructor
    public static class CaseClassOf2<T1, T2> implements MatchCase{
        Class<? extends CaseClass2<T1, T2>> cc;
        BiConsumer<T1, T2> bc;
        @Override
        public boolean match(Object o) {
            if(!cc.isInstance(o)) return false;
            Object[] param = Arrays.stream(cc.getDeclaredFields())
                .map(f -> {
                    f.setAccessible(true);
                    try {
                        return f.get(o);
                    } catch (IllegalArgumentException | IllegalAccessException ex) {
                        return null;
                    }
                })
                .toArray();
            bc.accept((T1)param[0], (T2)param[1]);
            return true;
        }
    }

あとは、このクラスのオブジェクトを返すメソッドを用意します。

    public static <T1, T2> CaseClassOf2<T1, T2> caseOf(
            Class<? extends CaseClass2<T1, T2>> cc, BiConsumer<T1, T2> bc)
    {
        return new CaseClassOf2<>(cc, bc);
    }

そしたら、ケースクラスなクラスを用意しましょう。CaseClass2インタフェースを実装して、それぞれの型を指定する必要があります。

    @AllArgsConstructor
    public static class Foo implements CaseClass2<String, Integer>{
        String s;
        Integer i;
    }
    
    @AllArgsConstructor
    public static class Bar implements CaseClass2<String, String>{
        String s1;
        String s2;
    }

matchメソッドは、最初の単一オブジェクトのものが使えます。なので、次のように処理を書くことができます。trimメソッドを呼び出すなど、ちゃんと型が認識されて分解されているのがわかります。

    Stream.of("やあ", 123, true,
            new Foo("これこれ", 123), new Bar("あれ", "それ"))
            .forEach(item -> 
    {
        match(item,
            caseOf(Foo.class, (s, i) -> {
                System.out.printf("ふぅ、%sを%d%n", s.trim(), i);
            }),
            caseOf(Bar.class, (s1, s2) -> {
                System.out.printf("ばあ%sと%s%n", s1.trim(), s2.trim());
            }),
            caseOf(Boolean.class, b -> {
                System.out.println(b ? "真" : "偽");
            }),
            caseOf(Integer.class, i -> {
                System.out.println(i < 10 ? "小さい" : "大きい");
            }),
            caseOf(String.class, s -> {
                System.out.println("そのまま:" + s);
            })
        );
    });

今回はCaseClassOf2を用意しましたが、CaseClassOf32くらいまでをちくちくと用意すればいいんじゃないでしょうか。そんときは、Consumer32<T1, T2, ... , T32>のようなクラスも必要になります。自分で使う分には、CaseClassOf5くらいまででいいと思うけど。

要素がひとつのケースクラスの場合は、メソッド解決があいまいになるかもしれないので、caseOfじゃないメソッド名にする必要があるかもしれません。


で、ケースクラスにいちいちinterfaceを実装して型を指定するのはカッコ悪いんですが、これはアノテーションプロセッサで自動的に付加できるんじゃないかと思います。

そうすると、次のような記述でよくなります。

    @CaseClass
    public static class Foo{
        String s;
        Integer i;
    }
    
    @CaseClass
    public static class Bar{
        String s1;
        String s2;
    }

できるのかな?


まとめ

案外、使い物になりそうなパターンマッチが実装できました。パターンを入れ子にできないのは、ちょっとつらいですけど。

問題点としては、まずパフォーマンスの問題があります。matchメソッドでいちいちパターンのインスタンスを生成するので、効率が悪いです。かといって、一度生成したものを使いまわすようにすると、記述性やみやすさが損なわれます。言語仕様に組み込んでもらって、invokeDynamic!とかが素敵な気がします。

記述のパターンが多くてわかりにくいのも問題です。今回は簡単な実装なので、caseOfメソッドでだいたいいけましたが、いろいろやっていくといろんな名前でcaseOfXxxみたいなのを用意する必要がでてきます。これも言語仕様に組み込んでもらって・・・


まあ、実用うんぬんは別として、こういうの実装するのは楽しいです。