uehaj's blog

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

ParrotブランチでGroovy3を垣間見る

これはG*Advent callender 2016の記事です。 前日はわたし@uehajの記事でした。明日は@mshimomuさんの記事です。

はじめに

Groovyの構文解析処理には初期からANTLRというパーサジェネレータのバージョン2(antlr2)が長らく使われてきたのですが、最近Daniel SunさんによってANTLRのバージョン4(antl4)への書き直しがなされました。この新パーサーは"Parrot"と呼ばれています。ParrotのGroovy本体への組込みは、Groovyソースコードの'parrot'ブランチで作業が進められています。

Parrotでは従来との互換を損なわないように全構文が実装されていることに加え、構文レベルでの新たな機能追加がなされており、本記事で紹介します。

Parrotは、構文解析の結果として従来と互換性のある中間コード(抽象構文木)を吐くように設計されており、parrotブランチではすでに実際に新パーサでGroovyコードを実行できるようになっています。

構造上parrotは旧版パーサとコンフリクトせずに同時に組み込んでシステムプロパティで切り換えて使うことができるようになっています。またsubprojectとしてモジュール化され分離されています。parrotが実際にGroovyに組込まれる予定はわかっておりません。記事タイトルは「Groovy 3」としましたが、利用可能になる時期はひょっとしたらもっと早いかもしれません。

Parrotブランチのコンパイル

以下の手順でParrotを試すことができます。

$ git clone https://github.com/apache/groovy.git
$ git checkout parrot
$ ./gradlew -PuseAntlr4=true -x groovydoc -x test installGroovy
$ export GROOVY_HOME=$(PWD)/target/install
$ export JAVA_OPTS=-Dgroovy.antlr4=true

$ $GROOVY_HOME/bin/groovy
$ $GROOVY_HOME/bin/groovysh
$ $GROOVY_HOME/bin/groovyConsole

警告

parrotはα以前の段階ですし、将来リリースされたときに仕様が本記事のままである保証は全くありません。また、さらに多数の機能拡張がなされていくことも予想され、期待されます。

do-whileループ

まずはこれ、Javaにはあるdo-whileループが従来Groovyにはありませんでしたが、利用できるようになってます。

groovy:000> do { println i++ } while (i<10)
0
1
2
3
4
5
6
7
8
9

do-while結構使いますよね。待望の、という感じです。

Java8構文

ParrotはJava 8で拡張された構文のいくつかに対応しています。

ラムダ式の記法

筆頭としてラムダ式です。Groovyでラムダ式の記法が使えるようになりました。*1

def list = [2, 3, 1]
Collections.sort(list, (n1, n2) -> n1 <=> n2)
assert [1, 2, 3] == list

今のところ、セマンティクスは従来のGroovyクロージャと同様です。parrotでのラムダ式のような記法「(a,b,c) -> body」は、クロージャ「{a,b,c -> body}」のシンタックスシュガーと思えばよいでしょう*2

それを確認するためにGroovyConsoleでinspect ASTしてみます。

f:id:uehaj:20161216004742p:plain

上記のように、コンパイル処理の早期段階である「conversion」ですでにクロージャに変換されていることがわかります。

なので、たとえばラムダ式記法を囲むメソッドのローカル変数をクロージャ中から変更することができますし、.apply()とか呼ばなくても()で呼べますし、delegateも機能しています(今のところ)。また、Java8のラムダ式のようにinvokeDynamicに変換されるということも(今のところは)無いでしょう。

細かい話1

Groovyのラムダ式は基本的にJava8のラムダ式に対して構文的には同等もしくは上位互換です。 たとえば、Groovyでも本体ブロックの括弧を省略したりできますし、さらにGroovyの特性としてreturn省略もできます。

(a,b) -> { return a+b }
(a,b) -> { a+b } // Groovyの仕様により許可(Javaではreturnが無いと怒られる)
(a,b) -> a + b // これならJavaでもreturn無しにできる。もちろんGroovyでもOK。
(int a, int b=0) -> a+b // デフォルト引数はGroovyならでは

しかしながら、わずかに例外もあります。 Javaでは引数が1個の場合、引数の括弧を省略できます。

(a) ->  a+1
a ->  a+1 // こう書ける。

しかし、parrotでは引数が1個でも括弧を省略できません。理由は、「{ a-> a+1 }」が、「クロージャ」なのか、「ラムダ式を含むブロック」なのかが構文解析上判別できなくなるからだそうです。=>なら区別できただろうに、先見的に->を使ってたのがかぶったという…。

細かい話2

クロージャで使えた暗黙の引数「it」はラムダ式では使えません。

groovy:000> [1,2,3].each ()->System.out.println(it)
ERROR groovy.lang.MissingMethodException:
No signature of method: groovysh_evaluate$_run_closure1.doCall() is applicable for argument types: (Integer) values: [1]
Possible solutions: doCall(), findAll(), findAll(), isCase(java.lang.Object), isCase(java.lang.Object)
groovy:000> [1,2,3].each (it)->System.out.println(it)
1
2
3
===> [1, 2, 3]

穏当です。

メソッド参照の記法

Java8のメソッド参照の記法が使えます。こちらも同様に、Groovyの.&演算子の適用のシンタックスシュガーで、つまり「System.out::println」は「System.out.&println」と同様です*3

ところで細かい話ですが、 Java8のメソッド参照で「クラス::インスタンスメソッド」と指定すると、第一引数がそのメソッドのレシーバであるような関数を返すのに、 今までのGroovyでは.&演算子で「クラス.&インスタンスメソッド」を指定したとき、そのメソッドに対してレシーバを渡す方法がなく呼び出すことができない、という違いがありました。

具体例を挙げます。Java8のメソッド参照では「クラス::インスタンスメソッド」を指定すると、

a = String::toUpperCase
assert a("abc") == "ABC"

のようにレシーバを第一引数で与えるように変換した関数を返すので、高階関数に渡したりするのにたいへん便利です。 しかし従来のGroovyでは「クラス.&インスタンスメソッド」を指定した場合、

groovy:000> a = String.&toUpperCase
===> org.codehaus.groovy.runtime.MethodClosure@2f67b837
groovy:000> a()
ERROR java.lang.IllegalArgumentException:
object is not an instance of declaring class
groovy:000> a("abc")
ERROR groovy.lang.MissingMethodException:
No signature of method: java.lang.String.toUpperCase() is applicable for argument types: (java.lang.String) values: [abc]
Possible solutions: toUpperCase(), toUpperCase(java.util.Locale), toLowerCase(), toLowerCase(java.util.Locale)

のようにエラーになって呼び出せませんでした。「インスタンス.&インスタンスメソッド」「クラス.&静的メソッド」は可能なのですがね。

parrotでは、Java8のメソッド参照形式でも「インスタンス::インスタンスメソッド」は当然できるし、ついでにGroovy形式の.&で「クラス.&インスタンスメソッド」を指定したとき

a = String.&toUpperCase
a("abc") == "ABC"

が可能になります。つまり.&はJava8のメソッド参照と同様のものになったということです。

コンストラクタ参照

こちらも可能となりました。勢い余ってかメソッド参照(.&)でも同じことが可能になりました。

groovy:000> s = String::new("abc")
===> abc
groovy:000> s = String.&new("abc")
===> abc

default method

これもかなりインパクトのあったJava8で導入された機能でした。

groovy:000> interface Foo { default getName(){ "foo" } }
===> true
groovy:000> class Bar implements Foo { }
===> true
groovy:000> new Bar().getName()
===> foo
groovy:000>

traitと同時にimplementsしたときどうなるのか、とか興味深いですが試してません。 ちなみにJava8ではインターフェースでstaticメソッドが定義可能になりましたが、それはまだ実装されていないようです。

Java7対応

try-with-resources

できます!

% cat a.groovy
class Resource implements Closeable {
  void close() { println "close" }
}

try (r = new Resource()) {
  println "hello"
  throw new Exception()
} catch (Exception e) {
  println "catch"
}
% $GROOVY_HOME/bin/groovy a.groovy
hello
close
catch

新しい演算子たち

等値演算子(===, !==)

意味的には、===はObject.is(Javaでの==)、!==はその否定ですね。

groovy:000> "a"+"b"=="ab"
===> true
groovy:000> "a"+"b"==="ab"
===> false
groovy:000> "a"+"b" != "ab"
===> false
groovy:000> "a"+"b" !== "ab"
===> true

エルビス代入演算子(i.e. ?=)

xx  = xx ? : 3

の略記法として、

xx  ?= 3

と書けます。「変更しようとする変数に値が設定されていなかったら設定する(先勝ち)」です。こんな動きをします。

groovy:000> def foo(xx) { xx ?= 3; println xx }
===> true
groovy:000> foo(null)
3
===> null
groovy:000> foo(5)
5
===> null
groovy:000> foo(0)
3
===> null

GroovyTruthで判定するので0は負けます。

!in, !instanceof

これは、私にとって今回の一番のお気にいり演算子です。

groovy:000> 3 !in [1,2,4]
===> true
groovy:000> 3 !in [1,2,3,4]
===> false
groovy:000> "a" !instanceof Integer
===> true
groovy:000> "a" !instanceof String
===> false

読みやすく書きやすい。

参考資料

以下の前者は記事をほぼ書きおわった後発見してちょっとショック。

まとめ

Groovyは本来Javaの上位互換言語であり、「任意のJavaコードは(ほぼ)Groovyコードでもある」が売りの一つでした。今回のパーサで導入されたJava8、Java7 の互換向上機能は、しばらくの間、いくぶん損なわれてしまっていたそれらのメリットを再度取り戻すものであるでしょう。また、今後の機能拡張の基盤ともなるものです。

コミッターも増え、Apache Groovy開発が活性化してきております。来年の発展が楽しみです。

プログラミングGROOVY
プログラミングGROOVY
posted with amazlet at 16.12.15
関谷 和愛 上原 潤二 須江 信洋 中野 靖治
技術評論社
売り上げランキング: 339,124

*1:昨日の記事の伏線はこれです。

*2:だから、「parrotではJava8のラムダ式を導入した」は言いすぎだと思う。穏当なのは「Java8のラムダ式の記法でもクロージャが記述できるようになった」ですかね。

*3:なので内部的にMethodHandleを使ってないので、これもJava8のメソッド参照とは構文上酷似した別モノ、ってことになるのかもしれない。

List.collectManyで非決定計算

これはG*Advent callender 2016の記事です。 前日は@Ziphilさんの記事でした。明日はまた私@uehajの記事です。

出オチでタイトルオンリーです。

すごいHaksell楽しく学ぼう(以降H本)にも書かれているように、リストはモナドとみなすと「非決定計算」をあらわすものとみることができます。

つまりたとえばリスト[1,2,3]を「1か2か3のどれか」、リスト[2,3]を「2か3のどれか」をそれぞれあらわすもの、と見るってことです。その上で、[1,2,3] * [2,3] という操作を何等かの方法で表現すれば、「1か2か3のどれか」に「2か3のいずれか」を掛けたものという意味になり、結果として「1*2,2*2,3*2,1*3, 2*3,3*3]すなわち「1か4か6か3か6か9のどれか」を得ることができます。

さて、Groovyのリスト(厳密にはIterable)には、collectManyというメソッドがあって、これは上記のようにリストを非決定計算と考えたときの、任意の計算を「何等かの方法」で適用すること*1に相当します。

やってみます。

H本の例

ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)  
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]  

ここでは「1,2のどっちか」と「'a','b'のどっちか」をタプル化操作したもののどれか( [(1,'a'),(1,'b'),(2,'a'),(2,'b')] )を得ています。 これをGroovyで考えると以下のようになります。

groovy:000> [1,2].collectMany{n -> ['a','b'].collectMany{ ch -> [[n, ch]] }}
===> [[1, a], [1, b], [2, a], [2, b]]

Haksellと同様の結果が得られます(Groovyにはちゃんとしたタプルが無いのでリストで代用)。

ピタゴラス数を求める

次に同様にcollectManyを使ってピタゴラス数を求めてみましょう。

ピタゴラス数とは、a^2 + b^2 == cを満たす3つの数です。aが10までの範囲で求めてみます。

(1..10).collectMany{ a ->
(1..a).collectMany{ b ->
(a..a+b).collectMany{ c ->
    return (a**2 + b**2 == c**2) ? ["a=$a, b=$b, c=$c"] : []
}}}.each {
    println it
}

実行すると

a=4, b=3, c=5
a=8, b=6, c=10

を出力します。

覆面算を解く

もう一つ応用例です。以下のような有名な覆面算を解いてみます。

   SEND
+) MORE
~~~~~~~~~~
  MONEY

上記を成立させるように、アルファベットに重複せずに数を割り当てる問題です。

この問題は、Sが「1〜9のどれか」Eが「1〜9のどれか、かつSではない」…という可能性をもった非決定的な数同士であり、それらがある条件を満すかどうかを判定する非決定計算の問題とみることができます。 これをcollectManyを使って解くと以下のようになります。

def digits = 0..9 as List

def isAnswer(S,E,N,D,M,O,R,Y) {
  (S*1000+E*100+N*10+D) + (M*1000+O*100+R*10+E) == (M*10000+O*1000+N*100+E*10+Y)
}

(digits-0).collectMany { S ->
(digits-S-0).collectMany { M->
(digits-S-M).collectMany { E->
(digits-S-M-E).collectMany { O->
(digits-S-M-E-O).collectMany { N->
(digits-S-M-E-O-N).collectMany { R->
(digits-S-M-E-O-N-R).collectMany { D->
(digits-S-M-E-O-N-R-D).collectMany { Y->
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}}}}}}}}.each {
 println it
}

上記は以下を出力します。

  9567
+)1085
 10652

メタクラスさん登場

collectManyがちょっと字面上うるさいという場合、

List.metaClass.rightShiftUnsigned = { x->delegate.collectMany(x) }

のように演算子「>>>」に割り当てても良いかもしれません。

List.metaClass.rightShiftUnsigned = { x->delegate.collectMany(x) }

def digits = 0..9 as List

def isAnswer(S,E,N,D,M,O,R,Y) {
  (S*1000+E*100+N*10+D) + (M*1000+O*100+R*10+E) == (M*10000+O*1000+N*100+E*10+Y)
}

(
(digits-0) >>> { S ->
(digits-S-0) >>> { M->
(digits-S-M) >>> { E->
(digits-S-M-E) >>> { O->
(digits-S-M-E-O) >>> { N->
(digits-S-M-E-O-N) >>> { R->
(digits-S-M-E-O-N-R) >>> { D->
(digits-S-M-E-O-N-R-D) >>> { Y->
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}}}}}}}}).each {
 println it
}

現場からは以上です。

余談

あとは余談ですが、「}}}}}}}}.each {」の閉じ括弧の連続が嫌ですね。これはGroovyのクロージャの記法{->}の作りが良くなくて、今回の用法ではネストが必要になるので、複数の閉じ括弧が必要になるというわけです。ここをJava8のラムダ式の記法で書ければ良いと思いませんか? つまり

(
(digits-0) >>> (S) ->
(digits-S-0) >>> (M) ->
(digits-S-M) >>> (E) ->
(digits-S-M-E) >>> (O) ->
(digits-S-M-E-O) >>> (N) ->
(digits-S-M-E-O-N) >>> (R) ->
(digits-S-M-E-O-N-R) >>> (D) ->
(digits-S-M-E-O-N-R-D) >>> (Y) -> {
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}).each {
 println it
}

と書く。やってやれないことはない、どうしたら良いのか?ということは明日の記事に回したいと思います(伏線)。

参考

uehaj.hatenablog.com

すごいHaskellたのしく学ぼう!
Miran Lipovača
オーム社
売り上げランキング: 60,257

*1:Haskellで言うモナディックな操作適用演算子>>=(bind)、Scalaで言うflatMap。

Javaにも不変データ構造に基づいた(Cons セルベースの)リストがあるのだよ

TL;DR

JDK(tools.jar)中、com.sunパッケージ配下に、javacが内部的に使っている「com.sun.tools.javac.util.List 」が含まれており、これは不変データ構造としてのリストのように(Cons セルベースのリストのように)利用できる。

はじめに

ScalaのコードをGroovyやJavaなどに移植する際に、いつも悲しい思いをするのが不変データ構造に基づいたリスト処理ライブラリが見あたらない、ということでした。JDKの標準コレクションライブラリには、Scalascala.collection.immutable.Listのような、不変データ構造を用いて実装されているリストがありません。

ここで言う不変データ構造に基づいたリストとは、JDKjava.util.Collections.unmodifiableList()等で取得できるような変更禁止ビュー、つまり「要素追加などを禁止するバージョンのリスト」のことではなく、あるいはGrooovyの@Immutableによるイミュータブル指定でもなく、

「要素の追加は可能だが、その意味は、元のリストの変更ではなく、要素が追加された新しいリストを返す」

というもので、さらに実装として「リストを丸ごとコピーして追加して返す」のではなく、「追加前のリストと実体を共有することによって、メモリ利用効率を下げない(そして追加前のリストは不変であることも保証される)」ことが期待されます。もう少しぶっちゃけて言えば、Lispの「ConsセルベースのList」が欲しいのです*1

もちろん、サードパーティ製のものもあるにはあるでしょう。Functional javaのとかClojureのを使うとかはできるのかな。しかし基本的にはやみくもには外部依存を増やしたくはないものです。自分で作りたくもない。

com.sun.tools.javac.util.List

最近知ったのですが、少なくともjava8までのJDKには、com.sunパッケージ配下に「com.sun.tools.javac.util.List 」というクラスがあり、これがまさに求めるものです。ただし、com.sunパッケージにあることから判るように標準APIの一部ではなく、将来的に使える保証はありません。もし使う場合は、将来使えなくなるリスクを覚悟して使う必要があります。さらに、rt.jarではなくtools.jarに含まれているので、JREとしては使用できずJDKの元で使えるものです。javaコマンドから利用する場合、tools.jarをクラスパスに指定することが必要となるでしょう。

※ @kmizuさんにご指摘いただきましたが、これはコメントに記載があるようにGJCつまりGeneric Java Compiler由来のもので、確かこれはScalaのMartin Oderskyさんの関わっていたプロジェクトではないですか。ある意味ご縁があるわけです。

使い方

com.sun.tools.javac.util.Listの機能としてはこんな感じ。

意味(適当な表記) 書き方
nil List.nil()
car(a) a.head
cdr(a) a.tail
cons(a,b) b.prepend(a)
[a,b,c] List.of(a,b,c)

使用例

これを使えば、例えば2リストキューを用いた非破壊的キューもほらこのとおり(cf.20分でわかるPurely Functional Data Structures)。

import com.sun.tools.javac.util.List;
import java.util.function.*;

public class Queue<E> {
    public static class Tuple<E> {
        E value;
        Queue<E> queue;
        Tuple(E value, Queue<E> queue) {
            this.value = value;
            this.queue = queue;
        }
    }

    private List<E> front;
    private List<E> rear;

    public Queue(List<E> front, List<E> rear) {
        this.front = front;
        this.rear = rear;
    }
    public Queue() {
        this(List.nil(), List.nil());
    }

    long size() { return front.size() + rear.size(); }

    public String toString(){ return "front<"+front+rear.reverse()+">rear"; }

    public Queue<E> add(E e) {
        return new Queue<E>(front, rear.prepend(e));
    }
    public Tuple<E> remove() {
        if (front == List.nil()) {
            if (rear == List.nil()) {
                return new Tuple<E>(null, this);
            }
            return new Queue<E>(rear.reverse(), List.nil()).remove();
        }
        else {
            return new Tuple<E>(front.head, new Queue<E>(front.tail, rear));
        }
    }
}

Groovyでさらに

Groovyでは以下の点で使いやすいです。

  • tools.jarは標準でGroovy実行時のパスに入っている
  • 名前のかぶるListではなく、「import com.sun.tools.javac.util.List as ConsList」のようにリネームしてインポートできる。

なお、残念ながらGroovyでは右結合の演算子オーバーロードできないので、ScalaHaskellの::のようにカッコなしで要素繋げる表記を使用することはできません(AST変換を使わないかぎり)。

もっと、もっと…欲しいんじゃっ…!

連想リストのマップっぽく使えるようした物が欲しいですが、それは見つけられませんでした(実装するのはたぶん難しくない)。

Purely Functional Data Structures
Chris Okasaki
Cambridge University Press
売り上げランキング: 50,281
Purely Functional Data Structures
Chris Okasaki
Cambridge University Press
売り上げランキング: 242,093

*1:LispのListが実際に不変であるか、といえば、実はそうではない。rplaca/rplacdといった破壊的操作があるから。以下で紹介するjavac.tools.Listも同じ。だとすると「不変データ構造と呼ぶべきか」が怪しいといういか間違い。本稿では「不変データ構造のように使えるリスト」として記載。(本件、@yasushia さんにもご指摘いただきました。ありがとうございます。)

Groovyをソースからコンパイルすると♪

groovyのソースをとってくるじゃろ。

% git clone https://github.com/apache/groovy.git

コンパイルするじゃろ。

% cd groovy
% ./gradlew installGroovy

すると、頭の中であの音楽が鳴り始める♪

http://f.st-hatena.com/images/fotolife/u/uehaj/20160305/20160305192747_original.png?1457173945

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:完全にかは不明。ターゲット型推論について要調査。

渋谷JVMで「いまさら始めようGroovy」を話しました

昨日、渋谷JVM

d-cube.connpass.com

にて発表させていただきました。

アテンドいただきましたコミュニティのみなさん、ビズリーチさま、聞いてくださったみなさん、ありがとうございました。また懇親会ごちそうさまでした(たこ焼たいへんおいしくいただきました!)。

他の言語の発表も聞くのも、またLTもたいへん楽しく参加させていただきました。名だたる皆さんと並んでの発表ということで、気おくれもし、準備とかパネルとか大丈夫かとか不安でしたが、サポートいただきなんとか乗り越えることができました。それとすばらしい会場ですねあそこはまた。起伏あり浜辺もあるし。

以下発表資料であります。

発表時に、会場のみなさんに「Groovy使っている人どのぐらいいますかー」と聞いたところ、非常に多くの割合*1で使われていたのでビックリし「およびでない、失礼しましたー」と帰りたくなりましたが、少しだけでも知見が増すところがあれば幸いなのですが、いかがなものでしたでしょうか。

かさねて御礼申しあげます。 以下、いただいたレスポンスなど

togetter.com shigemk2.hatenablog.com yukung.hatenablog.com takudo.github.io

*1:Gradle効果でしょうか。

G*Magazine Vol.8に記事をかきました

G*Magazine Vol.8に、Groovy 2.3, Groovy 2.4β4までで導入された新機能の解説記事を書きました。

http://grails.jp/g_mag_jp/images/gmagjp_8.png

PDF版もありますが、ブラウザで見れる以下のリンクを紹介します。

出稿タイミングの都合で、2.4最新版には対応しておりませんので悪しからず。