記憶のくずかご

メモを書く 適当に書く まじめに書かない

Java8のラムダ式をわかりやすく解説

できるかどうかわからんがやってみる。

きっかけは会社の人がJava8のラムダ式は難しいと言っていたから。
確かに関数型言語をいきなりJavaから学ぶのは難しいんじゃないかな。

なぜ難しく感じるのかというと、Javaオブジェクト指向言語を前提に設計しているのに関数型言語の概念を無理矢理ねじ込んだから。 Javaは良くも悪くも互換性を大事にするから、既存の構成を崩さず関数型を利用できるようにすると使い勝手が悪くなる。 元からオブジェクト指向と関数型の両方の概念をベースに設計していたらもっとわかりやすくなってたと思うけど。

さて、本題。
Javaラムダ式を説明するにあたって、Groovyを比較にして説明しようと思う。
なぜGroovyか?Javaに近いし、ラムダ式の概念がわかりやすいから。

Groovyだとラムダ式はこう書ける。

// 定義
Closure increment = { x ->
    x + 1
}

// 呼び出し
increment(1)

Javaだとこうだ。

// 定義
Function<Integer, Integer> increment = (x) -> {
    return x + 1;
};

// 呼び出し
increment.apply(1);

まあ、ほとんど同じだ。
ただ、Groovyと違ってJavaにはapplyとかがあって、なんじゃこりゃって思うんじゃないだろうか。

GroovyではClosure型がラムダ式を構成する。そしてClosureはブロック{}のことだ。 このブロックってどこかで見たことがないだろうか。そう初期化子だ!...じゃなくてメソッドだ! つまり、メソッドの中身がClosureってことだ。メソッドの中身をオブジェクトとして使える、これが関数型言語なんだ。
なのでClosureはメソッドと同じように引数を受け取れる。コードで言うxがそれだ。 また、Closureを呼び出す場合もメソッドと何ら変わらない。引数に値を入れて呼び出すだけだ。

一方JavaはClosure型じゃなくてFunction型で、ジェネリクスを使ってたりする。 Groovyは動的型付けなので引数や戻り値の型は書かなくて良かった。 でも、Javaは静的型付けなので引数や戻り値の型は書かないといけない。
Groovyのラムダ式の構文はブロック{}と引数のための矢印->だけで良かったが、Javaはもう少し複雑だ。 引数に括弧()が必要なのとブロック{}の位置が違う。まあそれだけなん。
これがJavaラムダ式の基本形で、引数が一個だけだったら括弧()を省略できたり、文が一文だけだったらブロック{}とreturnを省略できたりする。GroovyはメソッドでもClosureでもreturn文を省略できるんだけどね。

そして、謎のapplyだ。これは一体なんなのか?
ここがJavaオブジェクト指向から関数型のパラダイムを可能にした際のひずみだと俺は思う。
Javaには関数を表現するオブジェクトなんてものはない。ただ単に関数型のインターフェースというものがあるだけだ。 そのインターフェースでラムダ式を表現しようとしてるだけだ。
つまり、上のJavaのコードはこういうことだ。

Function<Integer, Integer> increment = new Function<Integer, Integer>() {
    @Override
    public Integer apply(Integer x) {
        return x + 1;
    }
};

これはいわゆる無名クラスだ。つまり、Javaラムダ式の正体はなんてことはない、ただの無名クラスだったということだ。 それをもう少し簡潔に書ける文法で表現したのがラムダ式っていうものなんだ。 だからapplyっていうのはラムダ式で暗黙で作った無名クラスのapplyメソッドを呼んでいるだけなんだ。
こういったことから、Javaラムダ式はGroovyとは違って今のJavaの仕様に無理矢理当てはめたものという感じがしないでもない。

Javaのややこしさはもう少し続く。それはラムダ式を表現する型にある。 つまり、上で言うFunction型だ。これは一つの引数と一つの戻り値の関数を表す型である。 じゃあ、一つの引数は取るけど戻り値は必要ない場合はどうすりゃいいんだろうか。 もちろんFunction型で表現できなくもない、要は戻り値をVoid型にすればいいんだから。 でもJavaにはそれ専用の型があるんだよね、ややこしいことに。 それはConsumer型だ。
じゃあ、戻り値だけ受け取りたい場合は?Supplier型だ。引数を2つ受け取りたい場合は?BiFunction型だ。
...というように型がいっぱいある。一方GroovyではClosure型だけで表現できる。

以上のように、Groovyと比較することでラムダ式の理想(Groovy)と現実(Java)が見えてJavaラムダ式の理解が深まったかな?と思う。Javaも本当はGroovyみたいなラムダ式を書けるようにしたかったんだと思うんだけど、いかんせん互換性の問題からこんな形になってしまったんだと思う。

なんかGroovyを持ち上げてるような文章になってしまった。
別にC#と比較してもいいんだけど、C#は俺はわからん。 Lispだと説明が複雑になるしなあ。