uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

GroovyのクロージャとJava8のlambda式の違いについて

この両者は、似ているようでいて、基本的には別モノです。表にしてみます。

Groovyのクロージャ java8のlambda式
導入時期 2003年 2014年03月
ローカル変数へのアクセス 読み書き可能 実質的にfinal(変数そのものに対しては読み込みのみ)
実装方法 Closure型のインスタンス MethodHandle, invokeDynamic..
型推論の根拠 Closure<T>のTで返り値、@ClosureParamsで引数 FunctionalInterface(SAM型)
記法 { 引数 -> 本体 }
{ 本体 }
{-> 本体 }
(引数) -> { 本体 }
(引数) -> 式
() -> { 本体 }
暗黙の動的なthis delegateにより実現 -
性能 ローカル変数をenclosingするため間接参照にするためのオーバーヘッドあり 性能上のオーバーヘッド僅少

赤字がデメリット、青字が利点のイメージです。

このように、機能としては、Groovyクロージャの方が高機能だと言えます。特にローカル変数への書き込みアクセスが可能であることと、delegate名前空間を制御できるのがGroovyクロージャの強力な利点です。

他方、lambda式の方が優れているのは、一つは性能です。コンパイラが頑張っているのか、静的型の面目躍如でfor文にも負けぬパフォーマンスを叩き出すという話を聞いたことがあります。もともと並列化が目的だったようなので、遅くっちゃしょうがないのです。

もう1点、lambda式の方が優れている面があるのは、型推論に関することであり、本エントリのテーマでもあります。lambda式の方が型推論の効果を得るのが簡単です。

型推論に関して

どういうことかというと、たとえばクロージャもしくはlambda式を引数にとるメソッドsomethingがあったとします。
Javaなら

void something(Function<String, String> x) {
  System.out.println(x.apply("abc"));
}

こんな感じ、Groovyなら

void something(Closure<String> x) {
  println(x.apply("abc"))
}

です。これを呼び出すとき、それぞれ

something { String it -> it.toUpperCase() } // Groovy
something( (String it) -> { it.toUpperCase() } ) // Java8

これは問題ありません。しかし、以下のように型を指定せず、型推論を期待したとき、どうなるでしょうか。

something { it -> it.toUpperCase() } // Groovy
something( (it) -> { it.toUpperCase() } ) // Java8

Java

Javaでは問題ありません。somethingに渡す引数の型は、Functional Interfaceから判るからです。上であれば、クロージャ引数の型はFunction<String, String> xなので、引数の型が最初のString、戻り値の型は2番目の型変数でこれもStringです。

somethingを呼びだす時に渡すラムダ式の引数には、文字列型の値が渡ってくることがコンパイラは知ることができるので、itの型を省略してもStringであることが推論されます。

Groovy

Groovyでも動的Groovyであれば、すなわち@CompileStaticや@TypeCheckedを指定しなければ、問題ありません。.toUpperCase()が呼び出されるオブジェクトが実行時点でStringであれば良いからです。

しかし、静的Groovy、すなわち@CompileStaticや@TypeCheckedを指定した場合、クロージャの引数の型を省略した

    something { it -> it.toUpperCase() }  // Groovy

は静的型チェックについてのコンパイルエラーになります。

[Static type checking] - Cannot find matching method java.lang.Object#toUpperCase(). Please check if the declared type is right and if the method exists.
@ line 11, column 24.
something( { it -> it.toUpperCase() } ) // Groovy

これはコンパイル時に、クロージャ「{ it -> it.toUpperCase() }」の引数itの型がわからないからです。
Closure<T>というクロージャ引数から、Tは返り値の型でわかりますが、クロージャに渡される引数の情報はなく、引数の型がわかりません。

これをコンパイルエラーにならなくするにはクロージャの引数型を明示指定するか、あるいはsomethingがわを以下のようにします。

@TypeChecked
void something(@ClosureParams(value=SimpleType,options=['java.lang.String']) Closure<String> x) {
  System.out.println(x.call("abc"));
}

@ClosureParamsアノテーションで、クロージャに渡す引数の型を明示します。ここでは単純にString型をクラス名FQCN文字列で指定しています。他に、渡した他の引数の型や、ジェネリックスの型などを指定することもできます。

なんでこうなった

GroovyのクロージャがもともとFunctional Interfaceを前提としていないことがあります。その理由は、クロージャ導入当時は型推論なんかないので、クロージャの引数の型を指定する方法は不要だったからです。

では今なぜFunctional Interfaceを導入しないのか?

一つは、Groovyでは、引数に渡すクロージャの引数の型や個数によって、動作をかえることができるからとのことです。たとえば、Map.each()には、{k,v ->}というクロージャを渡すとkey,valueが、{e ->}というクロージャを渡すとMap.Entryが引数としてわたってきます。これをPolymorphic Closureと呼ぶそうです。この2つのいずれか、という直和型をGroovy/Javaの型システムはうまく表現できないでしょう。またこの理由に加え、おそらく後方互換性的な理由などもあって選択されなかったのではないかと想像しています。

いずれにせよ、他にもdelegate型推論の問題もあるので、仮にFunctional Interfaceを導入したとしても型推論の問題は全体としては解決しません。delegateの問題については一部それを解決しようとする@DelegatesToアノテーションに関するこちらの記事も参照ください。

ターゲット型推論

あと、Java8のlambdaの方ではターゲット型推論がたぶん良く効きます。その影響は、えーと良くわかりません。眠いから調査はやめときます。

相互運用

FunctionalInterfaceが求められる箇所でGroovyクロージャを使用すると、Closureのcall()を呼びだすようなlambda式が生成されてそれが渡されます。Groovy2.1だったかな?そこらでこれが可能となりました。実用上、これで良しとされています。Javaからlambda式をクロージャとして渡せるかというのはガリガリ書くことがたぶんできますが言語処理系のサポートはまったくありません。

まとめ

現在は、クロージャ引数を型推論させたいとき、つまり静的Groovyをしつつ型を省略したいときは、@ClosureParamsを使う必要があり、これはラムダ式における指定方法Functional Interfaceより煩雑ですが、ライブラリ側でしっかりとやれば、それを呼び出す側は簡潔かつ実行時型エラーフリー*1になります。GDKのクロージャをとるメソッド群ではしっかり指定がなされていますが、自分で定義する関数についても適用できます。

GroovyへのLambdaの導入

なお、過去のMLを見ると、GroovyでもJava8 lambdaを導入するってのはGroovy 3での開発予定(希望)項目ぐらいにはあがってたみたいです。しかし、もしやるとしても構文や意味、統合するのか併存か、互換性維持など、思いきりチャレンジングでしょうね。個人的には頭痛くなるので現状ぐらいでお腹いっぱいです。

参考

uehaj.hatenablog.com
uehaj.hatenablog.com
uehaj.hatenablog.com

*1:完全にかは不明。ターゲット型推論について要調査。