初めまして、田中と申します。"Groovy" と聞けば、まず「気分はグルービー」という青春漫画を思い出す、そんな世代の人間です。
さて、Groovy ですが、「Gradle を使うために仕方なく覚える言語」と思っていませんか?
私は、古いシステムの改修の一環として Ant を Gradle に置き換えることになり、最近 Groovy を使い始めたのですが、当初はそのように思っていました。しかし、実際に使ってみると、結構便利だし、なかなか興味深い言語だと感じます。とりわけ、すごいと思ったのが「AST 変換」です。
AST 変換とは
Groovy のコードは、最終的には Java バイトコードに変換されますが、ソースコードからいきなりバイトコードに変換されるわけではなく、まず文法構造をツリー形式で表した AST (Abstract Syntax Tree:抽象構文木) と呼ばる中間表現が作成されます。この AST を操作してプログラムの構造を書き換えるのが AST 変換です。
AST 変換はコンパイル時に行われるので実行時にオーバーヘッドがかからないというメリットがあります。
Groovy の AST 変換には「グローバル AST 変換」と「ローカル AST 変換」があります。プログラム全体を対象とするのがグローバル AST 変換、アノテーションを付けた要素だけを対象とするのがローカル AST 変換です。
ローカル AST 変換のためのアノテーションは、Groovy の API に標準で含まれています。例えば、アノテーション @Singleton ですが、
1 2 3 4 5 6 |
@Singleton class Foo { } Foo.instance // Foo の唯一のインスタンスがフィールド instance に格納されている new Foo() // RuntimeException が発生 |
クラスに付けるだけで AST 変換が行われ、そのクラスの唯一のインスタンスがフィールド instance に格納され、new でインスタンスを生成しようとすると RuntimeException が発生するようになります。
やってみた
とにかく、実際にやってみないと良く分からないので、
「メソッドの開始時と終了時に文字列(デフォルトで "ほげほげ" )と出力させる」
だけの全く無意味なローカル AST 変換を行うアノテーションを作成してみました。
まず、アノテーションのクラスです。
● HogeHoge.groovy
1 2 3 4 5 6 7 8 9 10 11 12 |
import java.lang.annotation.ElementType import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy import java.lang.annotation.Target import org.codehaus.groovy.transform.GroovyASTTransformationClass @Retention(RetentionPolicy.SOURCE) // クラスファイルにはAST変換の情報は不要 @Target([ElementType.METHOD]) // 対象はメソッドのみ @GroovyASTTransformationClass('HogeHogeTransformation') public @interface HogeHoge { String value() default 'ほげほげ' } |
AST 変換の実装クラスを指定するメタアノテーション @GroovyASTTransformationClass 以外は、Java で独自アノテーションを作る場合と同じです。
(注:@Target の引数が { } ではなく、[ ] になっているのは、Groovy の文法の都合です。)
次が AST 変換の実装クラスです。
● HogeHogeTransformation.groovy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import org.codehaus.groovy.ast.ASTNode import org.codehaus.groovy.ast.builder.AstBuilder import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.transform.ASTTransformation import org.codehaus.groovy.transform.GroovyASTTransformation // CompilePhaseについては後述 @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class HogeHogeTransformation implements ASTTransformation { @Override // astNodes[0] : アノテーションを表すノード // astNodes[1] : アノテーションがつけられた要素(今の場合はメソッド)を表すノード // sourceUnit : グローバルAST変換で使用(今は不要) public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { def (annotationNode, annotatedNode) = astNodes // アノテーションの引数を取得 // (デフォルト値はアノテーション側で指定した値を取得したかったが、できないようなので仕方なくここで設定) def value = annotationNode.getMember('value')?.value ?: 'ほげほげ' // メソッドの始めと終わりにprintlnを挿入 annotatedNode.code.statements.with { add(0, createPrintlnStatement(value)) add(createPrintlnStatement(value)) } } // 「println 文字列」というコードを表すノードを作成して返す def createPrintlnStatement(str) { new AstBuilder(). buildFromString( CompilePhase.SEMANTIC_ANALYSIS, false, // 戻り値のListの第2要素(全体を表すノード)が必要な場合はtrue "println '$str'" ).first() } } |
要するに、AST を探索するメソッド visit をオーバーライドして、アノテーションを付けたメソッドを表すノードの前後に「println 文字列」というコードを表すノードを挿入しているだけです。
以上です。では、試してみましょう。
● HogeHogeExample.groovy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// デフォルト値の場合 @HogeHoge def foo() { println 'foo' } // 値を指定した場合 @HogeHoge('ふがふが') def bar() { println 'bar' } foo() bar() |
【実行結果】 (Groovy 2.3.0, CentOS7)
1 2 3 4 5 6 7 8 |
> groovyc HogeHoge.groovy HogeHogeTransformation.groovy > groovy HogeHogeExample.groovy ほげほげ foo ほげほげ ふがふが bar ふがふが |
(注:AST 変換の実行時に HogeHoge.class と HogeHogeTransformation.class が作成されている必要があるので、予め groovyc でコンパイルしています。)
Compile Phase
上記プログラム中の CompilePhase.SEMANTIC_ANALYSIS について解説しておきます。
Groovy のコンパイルは 9 段階(phase)で実行され、それぞれの段階を指定するための Enum 型 CompilePhase が用意されています。
① INITIALIZATION (初期化)
② PARSING (構文解析)
③ CONVERSION (変換)
④ SEMANTIC_ANALYSIS (意味解析)
⑤ CANONICALIZATION (正規化)
⑥ INSTRUCTION_SELECTION (命令選択)
⑦ CLASS_GENERATION (クラス生成)
⑧ OUTPUT (出力)
⑨ FINALIZATION (終了)
③ の 「変換」とは、② でソースコードを分解したトークンで作成された構文木(まだ抽象化されていない)を AST に変換する、という意味です。上記プログラムでは、AST ができた後の状態に介入するので、③ の次の ④ SEMANTIC_ANALYSIS を指定している、というわけです。
最後に
なんとなく AST 変換について分かったような気になりました、よね?
勿論、実際に役立ち、かつ安全なコードを書くには、AST の構造についてやコンパイルの詳細について、もっと勉強しなければなりませんが......
現状では今ひとつパッとしない Groovy ですが、実は結構すごいヤツなんだ、ということが分かって頂ければ幸いです。
尚、以下を参考にさせて頂きました。
・http://glaforge.appspot.com/article/groovy-ast-transformations-tutorials で紹介されている記事
・関谷和愛・上原潤二・須江信洋・中野靖治 (2011)『プログラミングGROOVY』技術評論社