Groovy v1.8の新機能をサクっと紹介するよ
このシリーズの一覧はこちら
はじめに
待ちに待ったGroovy v1.8がやっと出ましたね!ということで、Groovy v1.8の新機能をサクっと紹介したいと思います。
Groovy v1.8は結構多くの機能拡張がなされておりますので、サラっと行きたいと思います。
コマンドチェーン(ドメイン固有言語:DSL)
Groovy v1.8以前まではトップレベルでのメソッド呼び出しの括弧は省略できませんでしたが、新しいコマンドチェーン機能を使えば括弧なしのメソッド呼び出しが可能になります。
これは、a b c d のように呼び出した場合は実際には a(b).c(d) と同等となるようなことです。複数メソッドやクロージャ、名前付き引数と一緒に機能して、さらに右側に連ねてゆくことができます。
新しい構文でサポートされている例を見てみましょう。
turn left then right // turn(left).then(right) と同様 take 2.pills of chloroquinine after 6.hours // take(2.pills).of(chloroquinine).after(6.hours) と同様 paint wall with red, green and yellow // paint(wall).with(red, green).and(yellow) と同様 // 名前付きパラメータも! check that: margarita tastes good // check(that: margarita).tastes(good) と同様 // クロージャも! given { } when { } then { } // given({}).when({}).then({}) と同様
引数なしの場合は括弧が必要です。
select all unique() from names // select(all).unique().from(names) と同様
もし、コマンドチェーンの要素数が奇数の場合、メソッド, 引数, …と続いて、最後はプロパティアクセスになります。
take 3 cookies // take(3).cookies と同様 // さらに take(3).getCookies() と同様
これをうまく利用するとこんなDSLが作れるようになる。詳細は解説しませんが、他の例はこちらにもっとたくさんあります。
show = { println it } square_root = { Math.sqrt(it) } def please(action) { [the: { what -> [of: { n -> action(what(n)) }] }] } please show the square_root of 100 // please(show).the(square_root).of(100) と同様 // 出力結果 ==> 10.0
以下は日本語メタプログラミングスタイルを使った例です。
Or if you prefer Japanese and a metaprogramming style (see here for more details): // Japanese DSL using GEP3 rules Object.metaClass.を = Object.metaClass.の = { clos -> clos(delegate) } まず = { it } 表示する = { println it } 平方根 = { Math.sqrt(it) } まず 100 の 平方根 を 表示する // First, show the square root of 100 // => 10.0
こちらについては
も参考にしてださい。
パフォーマンスの向上
v1.8ではメインで以下2つの最適化が行われている
- Integerの基本演算(加算・減算・乗算・インクリメント・デクリメント・比較)。このバージョンでは違った型を混合で使うことはできません。もし混合で使うと最適化は行われません。
- "this"でのメソッド呼び出し時の引数がぴったり一致する場合、メソッドはダイレクトに呼び出されるようになります。なので、これは実行時のメソッド動的呼出しの際には行われません。
これらはまだ始まりに過ぎません。v1.8.xでは同様の最適化が行われる予定です。特にInteger以外のプリミティブ型の対応はまもなく行われるでしょう。
GParsをGroovyの標準機能として取り込まれた
GParsは並行処理、非同期処理、分散処理などを行ってくれるライブラリで、Fork/Join, Map/Filter/Reduce, DataFlow, Actors, Agentsなどの機能が使えるようになります。詳しくはGParsのウェブサイトや詳細なユーザーズガイド、もしくはGroovy in Action, 2nd Edition(MEAP)の17章をチェックしてみて下さい。
クロージャの強化
クロージャはGroovyの中核を成す機能で、Groovy APIの中でも至る所で利用されています。ここではGroovy v1.8でアノテーションパラメータとしてクロージャを利用する方法をご紹介します。クロージャはGroovyに関数型の味わいを与える重要な部分となっています。
アノテーションパラメータ
Javaでは、アノテーションのパラメータとして使える型はString、プリミティブ型、アノテーションクラス、クラスとそれらの配列のみとなっています。しかし、Groovy v1.8ではクロージャをアノテーションのパラメータとして利用することができます(実際は互換性のため、内部的にクラスに変換されます)。
import java.lang.annotation.* @Retention(RetentionPolicy.RUNTIME) @interface Invariant { Class value() // クロージャクラス格納用 } @Invariant({ number >= 0 }) class Distance { float number String unit } def d = new Distance(number: 10, unit: "meters") def anno = Distance.getAnnotation(Invariant) def check = anno.value().newInstance(d, d) assert check(d)
例としてGContractsをご紹介します。GContractsは「契約による設計」のパラダイムをGroovyで使えるようにするもので、事前条件、事後条件、不変条件を宣言するため、アノテーションパラメータが多用されています。
クロージャで関数型の味わい
クロージャ合成
数学の授業で合成関数に慣れ親しんだのを思い出してみて下さい。それと同じようにクロージャ合成はクロージャ同士を合成して数珠繋ぎに呼び出して行く新しいクロージャを生成します。以下が実際に動作する例です。
def plus2 = { it + 2 } def times3 = { it * 3 } def times3plus2 = plus2 << times3 assert times3plus2(3) == 11 assert times3plus2(4) == plus2(times3(4)) def plus2times3 = times3 << plus2 assert plus2times3(3) == 15 assert plus2times3(5) == times3(plus2(5)) // 逆に合成 assert times3plus2(3) == (times3 >> plus2)(3)
さらに多くの例はテストケースをご参照下さい。
末尾再帰最適化(TCO)(トランポリン)
末尾再帰最適化ができるようになりました。
def factorial factorial = { int n, BigInteger accu = 1G -> if (n < 2) return accu factorial.trampoline(n - 1, n * accu) } factorial = factorial.trampoline() assert factorial(1) == 1 assert factorial(3) == 1 * 2 * 3 assert factorial(1000) == 402387260... // plus another 2560 digits
これについては、
がとても参考になります。
メモ化
副作用なしのクロージャを用意して、結果のメモ化ができるようになりました。
def plus = { a, b -> sleep 1000; a + b }.memoize() assert plus(1, 2) == 3 // after 1000ms assert plus(1, 2) == 3 // return immediately assert plus(2, 2) == 4 // after 1000ms assert plus(2, 2) == 4 // return immediately // other forms: // at least 10 invocations cached def plusAtLeast = { ... }.memoizeAtLeast(10) // at most 10 invocations cached def plusAtMost = { ... }.memoizeAtMost(10) // between 10 and 20 invocations cached def plusAtLeast = { ... }.memoizeBetween(10, 20)||<
この例だと、plusというクロージャはa,bという引数の組み合わせで結果が決まるため一度呼び出された引数の組み合わせを覚えておいて(メモ化して)もう一度呼ばれた際に再利用できるようにするという仕組みです。ただ、メモ化するにもメモリを消費しますので、最大x個までメモ化する、とか、最低でもx個メモ化するという指定もできます。
これについては、
がとても参考になります。
カリー化の改善
実はv1.7の頃から出来ていたのですが、あらためて紹介するとのことで。カリー化する際、通常は引数を左からカリー化して行くのですが、これを右から行ったり、指定位置を直接カリー化することができるようになりました。
// right currying def divide = { a, b -> a / b } def halver = divide.rcurry(2) assert halver(8) == 4 // currying n-th parameter def joinWithSeparator = { one, sep, two -> one + sep + two } def joinWithComma = joinWithSeparator.ncurry(1, ', ') assert joinWithComma('a', 'b') == 'a, b'
これについては、
もご参照下さい。
ネイティブJSONサポート
JSONのビルダーとパーサーを使うことで、ネイティブにサポートされるようになりました。
これについては、
もご参照下さい。
JSONの読み込み
import groovy.json.* def payload = new URL("http://github.com/api/v2/json/commits/list/grails/grails-core/master").text def slurper = new JsonSlurper() def doc = slurper.parseText(payload) doc.commits.message.each { println it }
JSONビルダー
import groovy.json.* def json = new JsonBuilder() json.person { name "Guillaume" age 33 pets "Hector", "Felix" } println json.toString()
出力結果
{"person":{"name":"Guillaume","age":33,"pets":["Hector","Felix"]}}
JSONの整形
import groovy.json.* println JsonOutput.prettyPrint('''{"person":{"name":"Guillaume","age":33,"pets":["Hector","Felix"]}}''')​​​​​​​​​​​​
整形された出力結果
{ "person": { "name": "Guillaume", "age": 33, "pets": [ "Hector", "Felix" ] } }
新しいAST変換
Groovyコンパイラはソースコードを読み込み抽象構文木(Abstract Syntax Tree: AST)を構成しバイトコードに変換します。AST変換を使うことで、プログラマにこの処理へのフックを提供できます。詳細はGroovy in Action, 2nd Edition (MEAP)の第9章などをご覧下さい。
以下がGroovy v1.8で使えるようになった変換のリストです。これらは同じようなコード記述を抑えたり、共通エラーを引き起こすようなコードを避けるようにしてくれます。
@Log
このアノテーションを付けると、自動的にGroovyクラスにlogというプロパティ名でロガーを仕込んでくれるようになります。ロガーは4種類利用可能です。
import groovy.util.logging.* @Log class Car { Car() { log.info 'Car constructed' } } def c = new Car()
@Field
スクリプトで宣言した変数をフィールドとして他のメソッドなどから呼び出せるようにしてくれます。
@Field List awe = [1, 2, 3] def awesum() { awe.sum() } assert awesum() == 6
@PackageScope (の強化)
パッケージスコープをクラスやメソッドやフィールドに与える。
@AutoClone
自動的に必要なメソッドを実装して、Cloneableなクラスにしてくれます。
import groovy.transform.AutoClone @AutoClone class Person { String first, last List favItems Date since }
は、以下のようなクラスに変換されます。
class Person implements Cloneable { ... public Object clone() throws CloneNotSupportedException { Object result = super.clone() result.favItems = favItems.clone() result.since = since.clone() return result } ... }
@AutoExternalizable
自動的に必要なメソッドを実装して、Externalizableなクラスにしてくれます。
import groovy.transform.* @AutoExternalize class Person { String first, last List favItems Date since }
は、以下のようなクラスに変換されます。
class Person implements Externalizable { ... void writeExternal(ObjectOutput out) throws IOException { out.writeObject(first) out.writeObject(last) out.writeObject(favItems) out.writeObject(since) } void readExternal(ObjectInput oin) { first = oin.readObject() last = oin.readObject() favItems = oin.readObject() since = oin.readObject() } ... }
@ThreadInterrupt
Threadがinterruptedされているかどうかのチェックを追加してくれます。
@ThreadInterrupt import groovy.transform.ThreadInterrupt while (true) { // eat lots of CPU }
@TimedInterrupt
指定した時間経過するとThreadをinterruptしてくれるコードを追加してくれます。
@TimedInterrupt(10) import groovy.transform.TimedInterrupt while (true) { // eat lots of CPU }
@ConditionalInterrupt
条件を満たすとThreadをinterruptしてくれるコードを追加してくれます。アノテーションクロージャパラメータで条件を指定します。また、参照する値として@Fieldを指定しています。
@ConditionalInterrupt({ counter++ > 2 }) import groovy.transform.ConditionalInterrupt import groovy.transform.Field @Field int counter = 0 100.times { println 'executing script method...' }
@ToString
toString()メソッドを自動で追加してくれます。
import groovy.transform.ToString @ToString class Person { String name int age } println new Person(name: 'Pete', age: 15) // => Person(Pete, 15)
オプション付きで指定した例
@ToString(includeNames = true, includeFields = true) class Coord { int x, y private z = 0 } println new Coord(x:20, y:5) // => Coord(x:20, y:5, z:0)
@EqualsAndHashCode
equalsとhashCodeメソッドを追加してくれます。
import groovy.transform.EqualsAndHashCode @EqualsAndHashCode class Coord { int x, y } def c1 = new Coord(x:20, y:5) def c2 = new Coord(x:20, y:5) assert c1 == c2 assert c1.hashCode() == c2.hashCode()
@TupleConstructor
タプルベースのコンストラクタを自動で追加してくれます。Groovyは通常の場合はマップベースのコンストラクタのみがデフォルトで利用可能です。
import groovy.transform.TupleConstructor @TupleConstructor class Person { String name int age } def p1 = new Person(name: 'Pete', age: 15) // マップベース def p2 = new Person('Pete', 15) // タプルベース(追加!) assert p1.name == p2.name assert p1.age == p2.age
@Canonical
@ToString、@EqualsAndHashCode、@TupleConstructorの機能の合成ですので、それぞれのメソッドが追加されます。
import groovy.transform.Canonical @Canonical class Person { String name int age }
@ToString、@EqualsAndHashCode、@TupleConstructorのうちで特定変換を追加すると、そこについては追加した内容が優先されます。
import groovy.transform.* @Canonical @ToString(includeNames = true) class Person { String name int age } def p = new Person(name: 'Pete', age: 15) println p // => Person(name:Pete, age:15)
@InheritConstructors
親のコンストラクタを追加してくれます。
class CustomException extends Exception { CustomException() { super() } CustomException(String msg) { super(msg) } CustomException(String msg, Throwable t) { super(msg, t) } CustomException(Throwable t) { super(t) } }
以下のようにシンプルに書くことで、上記と同様になります。
import groovy.transform.* @InheritConstructors class CustomException extends Exception {}
@WithReadLockと@WithWriteLock
この二つのアノテーションを使ってjava.util.concurrent.locksな機構を追加してくれます。
import groovy.transform.* class ResourceProvider { private final Map<String, String> data = new HashMap<>() @WithReadLock String getResource(String key) { return data.get(key) } @WithWriteLock void refresh() { //reload the resources into memory } }
は、以下のようなクラスに変換されます。
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReadWriteLock class ResourceProvider { private final ReadWriteLock $reentrantlock = new ReentrantReadWriteLock() private final Map<String, String> data = new HashMap<String, String>() String getResource(String key) { $reentrantlock.readLock().lock() try { return data.get(key) } finally { $reentrantlock.readLock().unlock() } } void refresh() throws Exception { $reentrantlock.writeLock().lock() try { //reload the resources into memory } finally { $reentrantlock.writeLock().unlock() } } }
@ListenerList
もしコレクション型のフィールドを@ListenerListで注釈した場合、Beanのイベントパターンで必要な全てが生成されます。これはPropertyChangeEventsの@Bindableの独立したEventTypeの一種のようなものです。
@ListenerListの簡単な使用法はList型のフィールドを注釈し、Listを総称型にすることです。この例ではMyListener型のリストを使います。MyListenerはメソッドが一つのインタフェースでパラメータとしてMyEventを受け取ります。
interface MyListener { void eventOccurred(MyEvent event) } class MyEvent { def source String message MyEvent(def source, String message) { this.source = source this.message = message } } class MyBeanClass { @ListenerList List<MyListener> listeners }
これで以下のようなメソッドが作成されます。
- + addMyListener(MyListener) : void
- + removeMyListener(MyListener) : void
- + getMyListeners() : MyListener[]
- + fireEventOccurred(MyEvent) : void
JDK7対応
JDK7対応はGroovy v1.9で完全対応が終わる予定ですが、v1.8で既にダイヤモンドオペレータを除くProject Coinの対応が完了しています。
詳しくは、
を参照下さい。
新たなDGM(DefaultGroovyMethods)のメソッド
DGM(GDK)にも新たなメソッドが追加されたようです。DGMについては
の記事も参考にして下さい。
count Closure variants
countの引数にClosureが渡せるようになりました。クロージャがtrueを返した数をカウントしてくれるみたいですね。
def isEven = { it % 2 == 0 } assert [2,4,2,1,3,5,2,4,3].count(isEven) == 5
countBy
こちらはgroupByした結果のカウント数を保持してくれるようです。
assert [0:2, 1:3] == [1,2,3,4,5].countBy{ it % 2 } assert [(true):2, (false):4] == 'Groovy'.toList().countBy{ it == 'o' }
plus variants specifying a starting index
リストで要素を追加(plus)する位置をindexで指定できるようになったみたいです。
assert [10, 20].plus(1, 'a', 'b') == [10, 'a', 'b', 20]
equals for Sets and Maps now do flexible numeric comparisons (on values for Maps)
SetとMapの数値比較が柔軟になったようです。
assert [1L, 2.0] as Set == [1, 2] as Set assert [a:2, b:3] == [a:2L, b:3.0]
toSet for primitive arrays, Strings and Collections
Setに変換してくれるtoSetがプリミティブ配列と文字列とコレクションにも付いたようです。
assert [1, 2, 2, 2, 3].toSet() == [1, 2, 3] as Set assert 'groovy'.toSet() == ['v', 'g', 'r', 'o', 'y'] as Set
min / max methods for maps taking closures
Mapのminメソッド、maxメソッドにClosureが渡せるようになりました。Closureが返す値でmin, maxを判定してくれるんですね。(こちらはv1.7から既に使えました)
def map = [a: 1, bbb: 4, cc: 5, dddd: 2] assert map.max { it.key.size() }.key == 'dddd' // キーのサイズがの最大のキー assert map.min { it.value }.value == 1 // 値が最小の値
map withDefault{}
Mapでキーに対応する値がまだ格納されていなかった場合のデフォルト値が指定できるようになりました。
以下の例は、各単語の出現数をカウントする例ですね。
def words = "one two two three three three".split() def freq = [:] words.each { if (it in freq) freq[it] += 1 else freq[it] = 1 }
withDefaultを使うと、以下のように書けるようになりました。
def words = "one two two three three three".split() def freq = [:].withDefault { k -> 0 } words.each { freq[it] += 1 } これについては、[http://d.hatena.ne.jp/uehaj/20100608/1275951180:title]あたりを参考にしていただくと理解が早いです。
その他
スラッシュ文字列
スラッシュ文字列が複数行でも使えるようになりました。
def poem = / to be or not to be / assert poem.readLines().size() == 4
これは特に複数行で正規表現をフリースペースコメントスタイルで使いたい場合などに便利です(でもまだスラッシュをエスケープする必要がある)。
// match yyyy-mm-dd from this or previous century def dateRegex = /(?x) # enable whitespace and comments ((?:19|20)\d\d) # year (group 1) (non-capture alternation for century) - # seperator (0[1-9]|1[012]) # month (group 2) - # seperator (0[1-9]|[12][0-9]|3[01]) # day (group 3) / assert '04/04/1988' == '1988-04-04'.find(dateRegex) { all, y, m, d -> [d, m, y].join('/') }
ドルスラッシュ文字列
これは複数行のGStringに似ている新しい文字列表記です。違いはスラッシュをエスケープしなくてよくなったところです。もし必要なら、'$$' で '$' が、'$/' で '/' がエスケープできます。
def name = "Guillaume" def date = "April, 21st" def dollarSlashy = $/ Hello $name, today we're ${date} $ dollar-sign $$ dollar-sign \ backslash / slash $/ slash /$ println dollarSlashy
これは自然にスラッシュやバックスラッシュを含んでいるような文字列の表記に利用できます。
バックスラッシュ入り埋め込みXMLフラグメント
def tic = 'tic' def xml = $/ <xml> $tic\tac </xml> /$ assert "\n<xml>\ntic\\tac\n</xml>\n" == xml
スラッシュをエスケープしなければならない通常のスラッシュ文字列(/xxx/)や、バックスラッシュをエスケープしなければならないトリプルウォートのGString (""") よりも良い感じですよね。
もしくはWindowsのパスを表す場合などにも便利です
def dir = $/C:\temp\/$
コンパイルカスタマイズ
CompilerConfigurationクラスを通して、Groovyコードのコンパイルを設定することができます。 CompilerConfigurationクラスにコンパイルの設定のためのcustomizers (org.codehaus.groovy.control.customizersパッケージ)が追加されました。customizersを使うことによって3種類の方法でカスタマイズできます。
- ImportCustomizerでデフォルトインポートの追加
- SecureASTCustomizerでスクリプトとクラスをセキュアにする
- ASTTransformationCustomizerでAST変換の適用
例えば、@Log変換を全クラスとスクリプトに適用させたいなら以下のようにします。
import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.* import groovy.util.logging.Log def configuration = new CompilerConfiguration() configuration.addCompilationCustomizers(new ASTTransformationCustomizer(Log)) def shell = new GroovyShell(configuration) shell.evaluate(""" class Car { Car() { log.info 'Car constructed' } } log.info 'Constructing a car' def c = new Car() """)
デフォルトインポートを追加したいなら以下のようにします。
import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.* def configuration = new CompilerConfiguration() def custo = new ImportCustomizer() custo.addStaticStar(Math.name) configuration.addCompilationCustomizers(custo) def shell = new GroovyShell(configuration) shell.evaluate(""" cos PI/3 """)
(G)String to Enum coercion
文字列からEnumへ強制変換できるようになりました。
enum Color { red, green, blue } // asによる強制変換 def r = "red" as Color // 明示的な変換 Color b = "blue" // GStringでも同じようにできる def g = "${'green'}" as Color
マップでisCase()をサポート
MapでisCase()が使えるようになったので、以下のようなswitch/case文などで利用できます。
def m = [a: 1, b: 2] def val = 'a' switch (val) { case m: "key in map"; break // equivalent to // case { val in m }: ... default: "not in map" }
Grape/Grabの向上
@GrabResolverの略記法。特別なGrabリゾルバを書かなければならない場合、アーティファクトがMavenセントラルに格納されていない場合、以下のように書けました。
@GrabResolver(name = 'restlet.org', root = 'http://maven.restlet.org') @Grab('org.restlet:org.restlet:2.0.6') import org.restlet.Restlet
Groovy v1.8では以下のように書ける略記法が追加されました。
@GrabResolver('http://maven.restlet.org') @Grab('org.restlet:org.restlet:2.0.6') import org.restlet.Restlet
Grab属性オプションのコンパクト記法
@Grabアノテーションは沢山のオプションを持っています。たとえば、Apache commons-ioライブラリを使いたい場合は以下のような感じになります。
@Grab(group='commons-io', module='commons-io', version='2.0.1', transitive=false, force=true)
コンパクト記法では追加属性は文字列で指定できるようになりました。
@Grab('commons-io:commons-io:2.0.1;transitive=false;force=true') @Grab('commons-io:commons-io:2.0.1;classifier=javadoc') import static org.apache.commons.io.FileSystemUtils.* assert freeSpaceKb() > 0
Sqlクラスの向上
groovy.sql.SqlクラスのeachRowとrowsメソッドでページングがサポートされました。
sql.eachRow('select * from PROJECT', 2, 2) { row -> println "${row.name.padRight(10)} ($row.url)" }
結果として、2行目から始まって最大で2行が返されます。データベースにプロジェクト名とURLが入った多くのデータがある場合、以下のような感じの出力が得られるでしょう。
Grails (http://grails.org) Griffon (http://griffon.codehaus.org)
ASTノードメタデータの格納
AST変換を開発している際、特にASTノードのVisitorを使っている場合、訪れたツリー情報を保持しておくのは時々厄介です。そこで、ASTノードのベースクラスにメタデータを格納する機能を持った4メソッドが追加されました。
- public Object getNodeMetaData(Object key)
- public void copyNodeMetaData(ASTNode other)
- public void setNodeMetaData(Object key, Object value)
- public void removeNodeMetaData(Object key)
GroovyDocテンプレートのカスタマイズ機能
GroovyDocはハードコードされたテンプレートを使ってGroovyクラスのJavaDocを生成します。3つのテンプレート(トップレベルテンプレート、パッケージレベルテンプレート、クラステンプレート)が使われています。
もし、この3つのテンプレートをカスタマイズしたい場合は、Groovydoc Antタスクをサブクラス化して、getDocTemplates()、getPackageTemplates()、and getClassTemplates()メソッドをオーバーライドして自分のテンプレートとして使うことができます。
おわりに
というわけで、新機能がこれでもかと盛り込まれています。すごいなGroovy v1.8。
Enjoy making your programming life more groovier !!!!
更新履歴
- 2011-05-14 @ListenerListの説明を追加。