読者です 読者をやめる 読者になる 読者になる

100%ピュアJavaでeachやcollectを実現してみたら、ワリと普通だった...

java

はじめに

100%ピュアJavaでダックタイピングを実現してみたら、違う何かになっていた・・・ - 豆無日記
のダックタイピングに引き続き、rubyっぽい何かをJavaでやるとどうなるのかと妄想したシリーズ第2弾。

今度は、eachやcollectで有名なイテレーション処理について。

拡張for文

Java5から導入された拡張for文ってすげー便利ですよね。
Genericsとの相性も抜群だし。コレ使うためだけにJava5にする価値がある(ちょっと言いすぎ)。

    List<String> listObject = Arrays.asList("A", "B", "C");
    for (String it : listObject) {
        System.out.print(it);
    }

流れるようなインタフェースでeachを書く

最近はやりの流れるようなインタフェースで書けるようにしてみた。

基本文法はこんな感じ。

    For.in(対象コレクション).each(処理を実装したオブジェクト)

"対象コレクション"は、Iterableインタフェースを実装したものならなんでもok。
"処理を実装したオブジェクト"は、基本はEvalインタフェースを実装したインスタンスを指定します。


実際はこんな風になります。

    List<?> listObject = Arrays.asList("A", "B", "C"); // ここの型情報はあてにして無いよってことであえて<?>で。
    For.in(listObject).each(new Eval<String>() {
        public void eval(String it) {
            System.out.print(it);
        }
    });

なんか行数増えて複雑になっただけのように思えますね。
実際この程度の処理なら拡張for文で十分ですね・・・。


もうちょっと処理が複雑になってきたら威力を発揮するのですよ。きっと。たぶん。


または、

    List<?> listObject = Arrays.asList("A", "B", "C");
    List<?> listObject2 = Arrays.asList("1", "2", "3");
    Eval<String> doIt = new Eval<String>() {
        public void eval(String it) {
            System.out.print(it);
        }
    };
    For.in(listObject).each(doIt);
    // "ABC"と出力される
    For.in(listObject2).each(doIt);
    // "123"と出力される

みたいに複数回同じ処理を別のコレクションに実行するときとか。


そんなことがあるかどうかは、今は思いつかないですが。

いつかきっとあることを信じて・・・。

流れるようなインタフェースでcollectを書く

同じく流れるようなインタフェースで書けるようにしてみました。

    List<?> listObject2 = Arrays.asList("1", "3", "5");
    List<Integer> collected = For.in(listObject2).collect(new Convert<String, Integer>() {
        public Integer convert(String it) {
            if (it.equals("3")) return null; // nullにするとcollectされない
            return Integer.valueOf(it);
        }
    });
    System.out.print(collected);
    assertEquals(Arrays.asList(1, 5), collected);

これまた割りと普通なんですが、使っているインタフェースがさっきと違い今度はConvertになってます。

インタフェース実装がうざいときには〜ダックタイピング〜

上記の2例では、EvalやConvertというインタフェースを使用しています。

もうちょっと柔軟に行こうゼ!ということで、この前のダックタイピングの機構を利用して、

    List<?> listObject = Arrays.asList("A", "B", "C");
    For.in(listObject).each(new Object() {
        @SuppressWarnings("unused")
        public void eval(String it) { // @SuppressWarnings("unused")がないと未使用警告が出てしまう。無視してもいいんだけど。
            System.out.print(it);
        }
    });

などともかけるようにしてみました。

これだけだとEval使ったほうがええやんということになりそうですが、サンプルのテストクラスをみるとわかるように変態的な使い方ができたりするので、まああってもいいかなと。

実装的な何か

今回もそんなに実装上はたいしたことをしてません。
ネタ的にも良くある話だし、解としてもありふれてるので、既出率は高そうですね。

Lambda系インタフェース

あまり普通すぎてもつまらないので、今回ちょっと思い付きで以下のようなインタフェース構造にしてみました。

interface Lambda {

    interface Eval<T> extends Lambda {
        void eval(T it);
    }

    interface Convert<T1, T2> extends Lambda {
        T2 convert(T1 it);
    }
}

Lambdaなんか使わないでEvalもConvertもトップレベルのインタフェース(1インタフェース=1ファイル)にしてしまっても全然良いのだけれど、こうしておくと

  • Eval,ConvertはLambdaの内部インタフェースであることのメリット??
    • Eclipseで"Lambda"とか打ってから保管すると、Lambdaのサブタイプ(Eval,Convert)が保管候補として提示される
    • どうせ短いメソッド定義だし、同じ場所にあったほうが見通しが良い
      • ただし1つのインタフェースで2つのメソッドを定義すると、実装時に負担がかかるのでNG
  • Eval,ConvertはLambdaのサブタイプであることのメリット??
    • EclipseでCtrl+Tとかでクラス階層を表示させたとき、Lambdaのサブタイプ(Eval,Convert)も一覧に表示される

ということで、こういうのもありかもなぁぐらいの意図です。


コーディング規約として死守していくぜ!みんなも使えよこのやろう!とかそういう意気込みや思い込みなどは特にありません。


インターフェイス指向設計の変態インデントをみて、負けないぐらい変態的なことをしなくちゃ!とか思っていたのがつい出てしまったのかもしれません。


ちなみにインタフェース名がEvaluatable,Convertableではないのは、こういう類の機能はシンプルに書けることが命、だと思ったためです。
あえて短い名前で、しかも動詞のまま、としました。
普段の開発では省略せずにEvaluatable,Convertableとかっちり書くタイプです。ワリと真面目な感じです。


それから、Lambda1つでeval()とcollect()を定義しなかったのは、片方だけ使いたいのにもう片方のメソッドを空実装しないといけなくなるのがイヤだから。
デフォルト実装クラスのDefaultLambdaとか入れればいいなじゃいかって?そうすると名前が長くなってイケてないのと、クラスの継承の場合@OverrideをつけないとEclipseが警告をしてきやがるのがいやなのです。

"インタフェース"?"インターフェイス"?

ふと気づくと自分は"インタフェース"と呼んだり書いたりしますが、よく考えると英語的には"インターフェイス"の方があっているような。
インターフェイス指向設計を音読すれば、僕も"インターフェイス"派になれるかな。ママン。

まとめ

  • For-eachの制御構造は流れるようなインタフェースと相性がいいような気がした
  • 流れるようなインタフェースはまだまだ開発する余地がありそう(変態プレイ的な意味で
  • インターフェイス指向設計は買ったほうがイイ
  • ぎょ、業務に使おうと思ってヤッてんじゃないんだからねっ

ソース

CodeRepose上のjduckと同じところに入ってます。