G*Magazine Vol.8

Groovy臨機応変(第三回)〜Groovy 2.3の新機能〜

今回は、Groovy 2.3の新機能をピックアップして紹介します。詳しくはリリースノートを参照ください。

トレイト(Traits)

Groovy 2.3における目玉機能としてトレイトが導入されました。Groovyのトレイトは、実装の継承が可能なインターフェースです。Java8では、インターフェースにおいて、サブクラスで定義しなかった場合の「デフォルトのメソッド」を定義することができるようになりましたが、Groovyのトレイトも同種の機構であると言えます。しかし、Groovyのトレイトは、メソッド以外にもメンバー変数を定義・使用したり、Proxyを経由した動的な実装をすることができます。またJDK7以前のJava上でも利用することもでき、より強力な機構であると言えます。GroovyのトレイトはScalaのトレイトと似ています。詳しくは以下を参照ください。

(参考リンク)

新規AST変換の導入

@TailRecursive, @Builder, @Sortable, @SourceURIの4つのAST変換が導入されました。以下にそれぞれの説明を示します。

@TailRecursive

メソッドの自己再帰呼び出しをループに変換します。メソッドを跨った相互末尾再帰等には対応していないなどいくつかの制約があります。

以下の自己末尾再帰呼び出しを含むコードに@TailRecursiveを適用してみます。

@TailRecursive
def foo(int n, int lim) {
    if (n < lim) {
        foo(n+1, lim)
    }
    else {
        n
    }
}

上は以下のようにループに変換されます。

    @groovy.transform.TailRecursive
    public java.lang.Object foo(int n, int lim) {
        _lim_ = lim 
        _n_ = n 
        while (true) {
            try {
                if ( _n_ < _lim_ ) {
                    java.lang.Integer __n__ = _n_ 
                    java.lang.Integer __lim__ = _lim_ 
                    _n_ = __n__ + 1
                    _lim_ = __lim__ 
                    continue
                } else {
                    return _n_ 
                }
            } 
            catch (org.codehaus.groovy.transform.tailrec.GotoRecurHereException ignore) {
                continue
            } 
            finally { 
            } 
        }
        return null
    }

繰り返し数に比例してスタックを消費することがなくなるので、スタックオーバーフローを気にすることなく、適切な場合には再帰呼び出しでわかりやすくロジックを書くことができます。

(参考リンク)

@Builder

インスタンスを初期化するための、フルーエントなメソッド呼び出しによるビルダパターンを可能とするAST変換です。

使用例を以下に示します。

import groovy.transform.builder.Builder

@Builder
class Book {
    int price
    String title
}

def book = Book.builder().price(100).title("我輩は猫である").build()

上記以外に、いくつかのパターンでのサポートメソッドをAST変換の引数で指定することもできます。たとえば、setterをチェインさせていくようなパターンのビルダメソッドを生成することもできます。

(参考リンク)

@Sortable

指定したクラスに対して以下を行います。

  • compareToメソッドを生成し、インターフェースComparableを実装する。
  • それぞれのフィールドに関して比較するComparatorを返すメソッド(comparatorByXXX())を生成する。
    • →返されたComparatorは、java.util.Collections.sort(List list, Comparator c)や、java.util.Arrays.sort(T[] a, Comparator<? super T> c) に渡してソート処理に適用できる。

コード例を以下に示します。

import groovy.transform.Sortable

@Sortable
class Book {
    int price
    String title
}

data = [new Book(price:10, title:"abc"),
        new Book(price:9, title:"def")]

Collections.sort(data)
assert data[0].price == 9 && data[0].title == "def"
assert data[1].price == 10 && data[1].title == "abc"

(参考リンク)

@SourceURI

StringやURLクラス型の変数定義に指定すると、その変数の値にGroovyスクリプト自身のURIが初期設定されます。

import groovy.transform.SourceURI

@SourceURI String src

println src

上記のスクリプトを、ファイルに保存して実行するとfile:で始まるURIが表示されます。あるいは、スクリプトをWebサーバに置き、URLを指定してgroovyコマンドを実行すると、そのURLが表示されます。さらに、以下のように-eオプションでスクリプトを指定したり、GroovhShell.evaluate(String)でスクリプトを実行すると、@SourceURIはdata:で始まるデータURIを生成します。

$ groovy -e 'import groovy.transform.SourceURI; @SourceURI String src; println src'
data:,import%20groovy.transform.SourceURI;%20@SourceURI%20String%20src;%20println%20src

(参考リンク)

ライブラリの拡張・改善

マークアップテンプレートエンジン

新種のテンプレートエンジン、マークアップテンプレートエンジン(groovy.text.markup.MarkupTemplateEngine)が追加されました。マークアップテンプレートエンジンは、DOM的な構造を記述するためのDSLの一種であるとも言えます。参考リンクにある例を以下に示します。

yieldUnescaped '<!DOCTYPE html>'                                                    
html(lang:'en') {                                                                   
    head {                                                                          
        meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')      
        title('My page')                                                            
    }                                                                               
    body {                                                                          
        p('This is an example of HTML contents')                                    
    }                                                                               
}      

これは以下のようなXMLを生成するとのことです。

<!DOCTYPE html><html lang='en'><head><meta http-equiv='"Content-Type" content="text/html; charset=utf-8"'/><title>My page</title></head><body><p>This is an example of HTML contents</p></body></html>

マークアップテンプレートエンジンは、MarkupBuilderと同様に、クロージャや疑似メソッドの呼び出しを使用してツリー構造を記述・構築し、文字列表現に落とすことができます。加えて、以下の特長があります。

  • 静的コンパイルで高速化
  • 静的型チェックが可能
  • 整形(インデント、エスケープ)機能付き
  • テンプレート記述を別ファイルにしてincludeなども可能

(参考リンク)

JSON Slurper

Groovy 2.3ライブラリではいくつかの性能改善がなされており、特に、JSON処理が従来より最大20倍高速化され、Javaベースのライブラリの中で最速の部類となっているとのことです。

JSON Slurperについては、以下のパラメータを指定できるようになりました。

パラメータ説明
INDEX_OVERLAY高速。REST呼び出し・WebSocketメッセージ・AJAXなどに適している。シリアライズされたオブジェクトの展開処理を特に高速化している。
LAX コメントや、クオート無しのマップのキーを許す。
CHAR_BUFFERint,date,longなどの貪欲(eager)なパース処理が特長。既存のSlurperの動作に近い。
CHARACTER_SOURCE2MBを越えるJSONファイルの処理に適する。

これらのパラメータは以下のように指定します(参考リンクに示したAPIリファレンスより引用)。

   parser = new JsonSlurper().setType( JsonParserType.INDEX_OVERLAY );

(参考リンク)

ConfigSlurperの拡張

Grailsで定義されていて利用可能であるような、prod/dev/testといった「環境」を、新たに定義し指定できるようになりました。

(参考リンク)

Java8,7の対応

Java8

実行環境としてJDK 8が公式にサポートされました。

Java7

Java 7のNIO2に対応しました。例えば、いくつかのGDKメソッドで、java.io.Fileの代りにjava.io.Pathクラスが使用できます。

以下はリリースノートからの例です。

path.withReader { Reader r -> ... }
path.eachLine { String line -> ... }
path.eachFileRecurse { Path p -> ... }
path << 'some content'
path << bytes
path.readLines()

JUnit 4対応

groovy.test.GroovyAssertに以下の静的メソッドが追加されました(GROOVY-6588)。これらは、従来よりGroovyTestCaseのインスタンスメソッドとしては対応するものが定義されていたのですが、JUnit4でテストクラスをGroovyTestCaseから継承させない場合に使用するための、静的メソッドが定義されたクラスgroovy.test.GroovyAssertでは定義されていませんでした。

メソッド説明
static void assertScript(String script) 文字列のスクリプトを実行結果をassertする
static boolean notYetImplemented(Object caller)試験の成功/失敗を逆転させて扱う(FailならPassとし、PassならFailとする。
static Throwable shouldFail(Class clazz, String script)文字列のスクリプトを実行し指定した例外が発生したらPassとする
static Throwable shouldFail(String script)文字列のスクリプトを実行し例外が発生したらPassとする

Closure引数に対する静的型チェック

従来、クロージャを引数としてとるメソッドにおいて、クロージャ型の引数の引数に型を宣言する方法がありませんでした。

例えば、

void foo(Closure clos) { // クロージャclosの「引数の型」を宣言する方法がない。
   clos("ABC")                // 引数closのクロージャには、常に1つの文字列が引数として与えられるのだが…。
}

というメソッドが定義されているとき、以下のコード:

@TypeChecked
def func() {
   foo { it ->
       it.toUpperCase() // 静的型エラー。itの型はObjectとみなされる。
   }
}

は、クロージャ引数itの型を知る方法が無く、itに対するtoUpperCase()の呼び出しが静的型チェックや静的コンパイルでエラーになるので、以下のように型を指定する必要がありました。

foo { String it ->
    it.toUpperCase()
}

しかし、fooの宣言時に@ClosureParamを以下のように使用することで、itの型は推論されるようになり、型を明示的に指定する必要がなくなります。

void foo(@ClosureParams(value=SimpleType.class, options="java.lang.String") Closure clos) {
   clos("ABC")
}

話はジェネリクスが入ってくるともうちょっとややこしくなります[1]。次の例です。

["a","b","c"].collect { it.toUpperCase() }

この場合、「クロージャの引数の型」は、レシーバのListの「要素の型」と一致していなければなりません。

collectは、GDKメソッドとして、DefaultGroovyMethods.java(DGM)中で、以下のように定義されています。

 public static <S,T> List<T> collect(Collection<S> self,
            @ClosureParams(FirstParam.FirstGenericType.class) Closure<T> transform) {
        return (List<T>) collect(self, new ArrayList<T>(self.size()), transform);
  }

「@ClosureParams(FirstParam.FirstGenericType.class) transform」では、クロージャの引数transformの引数の型は、第一引数(FirstParam)すなわちレシーバであるコレクションselfのジェネリックス型引数(S)である、と宣言しています。ちなみにTはクロージャの戻り値の型です。

@ClosureParamsを使用することで、ライブラリ側の定義は煩雑になりますが、それを呼び出すコードにおいて@TypeChecked、@CompileStaticはより賢く振舞うようになり、コードを簡潔にすること、及び動的Groovyコードに適用した際の修正を減らすことに貢献してくれます。

なお、Groovy 2.4 beta 4では、同様の指定が@DelegatesToアノテーションでも可能になりました。(GROOVY-6956)

(参考リンク)

  • http://melix.github.io/blog/2014/01/closure_param_inference.html
  • [1] Java8ではGenericsの引数型に対してアノテーションが付与できるようになったので、このような間接的な指定方法ではなく直接指定できるはずである。あるいはGroovy自体の拡張で同種の指定も可能であっただろう。しかしながら、Groovyのコンパイル・動作環境はJava8には限定されておらず、またClosureParamsを使用する主な場所はJavaで定義されたDGM(DefaultGroovyMethods.java)であるため、Java 7以前でも使用できるこの方法が採用された。

ツールの拡張

Groovyshの拡張と変更

Groovyshは着々と拡張されています。まず、補完が賢くなりました(GROOVY-6399,GROOVY-6395)。たとえば、マップのキーを補完できるようになりました。

% groovysh
groovy:000> m = [abc:123, def:456]
groovy:001> m.a [タブを押す]
any(   abc
groovy:001> m.ab [タブを押す]
groovy:001> m.abc // 補完される

この機能は、DOMツリーやASTなど、ツリーをたどりながら表示させる場合に絶大な効果を発揮します。

また、機能の変更として、groovysh中でのコマンド(load, edit, aliasなど)にはプリフィックス「:」が必要となりました。変数名や関数名と重なることがなくなります。 (GROOVY-6397)

% groovysh
groovy:000> :load test.groovy // 従来はコロン無しの「load」で実行していた

GroovyConsoleの拡張

GroovyConsoleには以下の拡張がなされています。

  • プリファレンスAPIを通じてフォントが指定できるようになったとのことです。ただしフォント指定のためのGUIはまだ用意されていません。(GROOVY-6303)
  • 選択範囲で実行(Run Selection)したときに、import文を意識してくれるようになりました。つまり、選択範囲にimport文が含まれていなくても、同じソース中の冒頭位置などに含まれているimportがあたかも選択範囲中にも指定されているかのように解釈して実行してくれます。
  • コメント・コメント解除の機能とショートカットキーが追加されました。(GROOVY-6459)

ドキュメントの改善

その他、ドキュメントが抜本的に改善されています。改善された内容は、現時点ではGroovyのβ版ドキュメントページで公開されていますが、将来的には公式サイトの内容に置換されます。

(参考リンク)

G*Magazine Vol.8

Groovy臨機応変(第三回)〜Groovy 2.3の新機能〜
Groovy臨機応変(第四回)〜Groovy 2.4の新機能〜
Gradleプラグイン探訪
Grails Plugin探訪 〜第9回 CodeNarc plugin〜
リリース情報 2014.11.29
G*Magazine Vol.8- 第6章