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のメソッド参照とは構文上酷似した別モノ、ってことになるのかもしれない。