渋谷JVMで「いまさら始めようGroovy」を話しました
昨日、渋谷JVM
にて発表させていただきました。
アテンドいただきましたコミュニティのみなさん、ビズリーチさま、聞いてくださったみなさん、ありがとうございました。また懇親会ごちそうさまでした(たこ焼たいへんおいしくいただきました!)。
他の言語の発表も聞くのも、またLTもたいへん楽しく参加させていただきました。名だたる皆さんと並んでの発表ということで、気おくれもし、準備とかパネルとか大丈夫かとか不安でしたが、サポートいただきなんとか乗り越えることができました。それとすばらしい会場ですねあそこはまた。起伏あり浜辺もあるし。
以下発表資料であります。
発表時に、会場のみなさんに「Groovy使っている人どのぐらいいますかー」と聞いたところ、非常に多くの割合*1で使われていたのでビックリし「およびでない、失礼しましたー」と帰りたくなりましたが、少しだけでも知見が増すところがあれば幸いなのですが、いかがなものでしたでしょうか。
かさねて御礼申しあげます。 以下、いただいたレスポンスなど
togetter.com shigemk2.hatenablog.com yukung.hatenablog.com takudo.github.io
*1:Gradle効果でしょうか。
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アノテーションに関するこちらの記事も参照ください。
相互運用
FunctionalInterfaceが求められる箇所でGroovyクロージャを使用すると、Closureのcall()を呼びだすようなlambda式が生成されてそれが渡されます。Groovy2.1だったかな?そこらでこれが可能となりました。実用上、これで良しとされています。Javaからlambda式をクロージャとして渡せるかというのはガリガリ書くことがたぶんできますが言語処理系のサポートはまったくありません。
まとめ
現在は、クロージャ引数を型推論させたいとき、つまり静的Groovyをしつつ型を省略したいときは、@ClosureParamsを使う必要があり、これはラムダ式における指定方法Functional Interfaceより煩雑ですが、ライブラリ側でしっかりとやれば、それを呼び出す側は簡潔かつ実行時型エラーフリー*1になります。GDKのクロージャをとるメソッド群ではしっかり指定がなされていますが、自分で定義する関数についても適用できます。
GroovyへのLambdaの導入
なお、過去のMLを見ると、GroovyでもJava8 lambdaを導入するってのはGroovy 3での開発予定(希望)項目ぐらいにはあがってたみたいです。しかし、もしやるとしても構文や意味、統合するのか併存か、互換性維持など、思いきりチャレンジングでしょうね。個人的には頭痛くなるので現状ぐらいでお腹いっぱいです。