初めに
Groovy とは?
Groovy とは、Java プラットフォーム上で動く動的型付け言語です。シンタックスがほとんど Java と変わらないので、Java を動的型付けにした言語と考えてもらえば良いと思います。Groovy から Java のコードを呼び出せるので、既存のあらゆる Java のライブラリがそのまま使えます。
動的な側面だけでなく静的コンパイルもサポートされているのが Groovy の特徴です。静的コンパイルによって、動的な機能 (ダックタイピングや実行時のメソッド追加など) が制限される代わりに、動的なメソッド呼び出しに関するオーバヘッドがなくなるため、実行速度が Java と大差なくなります。
AST 変換とは?
Groovy のソースコードをコンパイルすると、まず文法に則ってコードを木構造で表した中間表現に変換されます。これを 「AST (Abstract Syntax Tree)」と呼びます。その後、この AST が Java のバイトコードに変換されます。AST 変換とは、この AST をバイトコードに変換される前に書き換えてしまえる機能です。
例えば、あるフィールドのゲッターメソッドとセッターメソッドを自動生成することを考えてみます1。これを AST 変換を使わずに行おうとすると、実行時にメソッドを追加することになります。しかし、この方法ではメソッドの動的呼び出しによってオーバーヘッドが発生し、パフォーマンスが悪化します。しかし、AST 変換を用いて、AST の段階でゲッターとセッターを追加してしまえば、そのゲッターとセッターが書き込まれた状態でバイトコードが出力されるため、オーバーヘッドがかからず、最初からソースコードにゲッターとセッターが書かれている場合と同じパフォーマンスが得られます。
Java の Lombok を知っていれば、それの Groovy 版だと思ってもらえれば結構です。ただし、Groovy ではそれが比較的簡単に実装できてしまうのが特徴です。
以上のように AST 変換は非常に強力ですが、ソースコード上の (見た目の上での) 処理内容と実際の処理内容が異なってしまうため、諸刃の剣とも言えます。
標準で利用できる AST 変換
Groovy ではいくつかの便利な AST 変換が標準で使えるようになっています。例えば、@ToString
アノテーションをクラスにつけることで、AST 変換が行われ、フィールドの内容を列挙して文字列にして返す toString
メソッドがそのクラスに追加されます。
import groovy.transform.ToString
@ToString
class Student {
int number
String firstName
String lastName
}
Student student = new Student(number: 5, firstName: "Taro", lastName: "Suzuki")
println(student.toString())
このコードを実行すると Student(5, Taro, Suzuki)
と表示されます。
AST 変換の作り方
仕様の決定
AST 変換は自分で新たに作成することもできます。試しに 1 つ作ってみることにしましょう。ただ、非実用的すぎる AST 変換を作っても仕方ないので、以下のような仕様のものを作成したいと思います。
Groovy では、演算子の /
は BigDecimal
を返します。例えば、7 / 2
の評価結果は Java では 3 (int
型) ですが Groovy では 3.5 (BigDecimal
型) です。Java と同じように整数の商を得たいときは、intdiv
というメソッドを使って 7.intdiv(2)
のように書く必要があります。
この仕様ですが、Java に慣れているとたまに迷惑に感じることがあります。そこで、ソースコード中で使われている /
を intdiv
に書き換える AST 変換を作ってみましょう。
アノテーションの作成
まず、ソースコードのどの部分に AST 変換を施すかを表すアノテーションを作る必要があります。ここで作成したアノテーションが付加された部分が AST 変換の対象になります。
アノテーションの名前はそのまま Intdiv
にしてみます。
package ziphil.transform
import groovy.transform.CompileStatic
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)
@Target([ElementType.METHOD, ElementType.TYPE])
@GroovyASTTransformationClass(["ziphil.transform.IntdivTransformation"])
@CompileStatic
public @interface Intdiv {
}
Java で独自のアノテーションを作る場合とほとんど同じですが、1 つだけ違うのは @GroovyASTTransformationClass
アノテーションがあるところです。このアノテーションで、AST 変換の処理が記述されているクラスを指定します。ここでは ziphil.transform.IntdivTransformation
にしました。
また、メソッドを付加できる位置ですが、ここではメソッドとクラスにしておきました。メソッドに付加した場合はそのメソッド内に現れる /
を intdiv
に変え、クラスに付加した場合はそのクラスに定義されている全てのメソッド内の /
を intdiv
に変えるようにします。
AST 変換で用いる主要なクラスについて
実装の前に、AST 変換を行うコートを書くときに出てくるクラスについて軽く説明しておきます。
まず、IfStatement
や ForStatement
のような ~Statement
という名前のクラスは、Groovy の文に対応しています。IfStatement
と ForStatement
は、それぞれ if 文 と for 文の AST を表現しています。これらのクラスは、org.codehaus.groovy.ast.stmt
パッケージに定義されています。
次に、MethodCallExpression
や ConstantExpression
のような ~Expression
という名前のクラスは、Groovy の式に対応しています。ここに挙げた MethodCallExpression
は "string".startsWith("s")
のようなメソッド呼び出しの AST を表現しており、ConstantExpression
は "aaa"
や 3L
などのリテラルの AST を表現しています。これらのクラスは、org.codehaus.groovy.ast.expr
パッケージに定義されています。
最後に、MethodNode
クラスはメソッドの AST を、ClassNode
クラスはクラスの AST を表現します。また、ClassNode
は、ソースコード中の型 (変数宣言やメソッド引数などで書かれるもの) を AST 内で表現するのにも使われます。
AST 変換の処理の実装 I: クラスの作成
では、AST 変換を実際に行うコードを書いていきます。
まず、ASTTransformation
インターフェースを実装したクラスを作ります。このインターフェースは唯一の抽象メソッド visit
をもっているので、そこに AST を処理するコードを書いていきます。
このとき、@GroovyASTTransformation
アノテーションで AST 変換のどの段階でその処理を行うかを指定します。実は AST 変換は 8 つの段階に分かれて行われており、どの段階で記述した処理を行うかを選ぶことができるのですが、これについては別の機会で説明するとして、ここでは深入りしないことにします。今回は、CompilePhase.SEMANTIC_ANALYSIS
を指定しておきます。
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class IntdivTransformation implements ASTTransformation {
public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
// ここに処理を書く
}
visit
の第 1 引数には大きさ 2 の配列が渡されます。この第 0 要素は付加したアノテーション (今の場合は @Intdiv
) を表す AST で、第 1 要素はアノテーションが付加された方 (今の場合はクラスやメソッド) の AST になっています。第 2 引数は使わないので説明は省きます。
AST 変換の処理の実装 II: ユーテリティクラスの利用
さて、visit
の第 1 引数の第 1 要素に処理したい AST が格納されているわけなので、その中に含まれる二項演算子 /
の利用部分を intdiv
の呼び出しに置き換えるコードを書けば良いのですが、AST は非常に複雑な構造をしているので、正直大変です。そこで、Groovy が提供しているユーティリティクラスを使うことにしましょう。
ClassCodeExpressionTransformer
クラスは、visit~
というメソッドがたくさん定義されており、AST を表現するオブジェクトを引数に渡して呼び出すと、その中身を走査します。そして、中身に存在する全ての式 (メソッド呼び出しやリテラルなど) を引数にして transform
メソッドを呼び出し、その返り値に書き変えます。したがって、このクラスの transform
メソッドを上書きすれば、比較的簡単に式の変換が行えます。
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class IntdivTransformation extends ClassCodeExpressionTransformer implements ASTTransformation {
private SourceUnit sourceUnit
public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
// アノテーションが付加された部分の AST を取得する
AnnotatedNode parent = (AnnotatedNode)nodes[1]
// 第 2 引数をフィールドに保持する (必要なのか分からないが一応)
this.sourceUnit = sourceUnit
if (parent instanceof ClassNode) {
// アノテーションがクラスに付加された場合は、クラスの中身を走査する
// クラスに含まれる全ての式に対して、下の transform メソッドが呼び出される
visitClass(parent)
} else if (parent instanceof MethodNode) {
// アノテーションがメソッドに付加された場合は、メソッドの中身を走査する
visitMethod(parent)
}
}
public Expression transform(Expression expression) {
if (expression != null) {
// ここに処理を書く
// expression の中身に対しても変換を行いそれを返す
return expression.transformExpression(this)
} else {
return null
}
}
public SourceUnit getSourceUnit() {
return this.sourceUnit
}
}
visit
の第 2 引数である sourceUnit
をフィールドして保持していますが、Groovy が標準で提供する AST 変換のコードを見るとどれもこうしているため、それを真似しただけで、これの意味は正直よく分かりません。
また、expression.transformExpression(this)
ですが、式はネストしている (2 + (4 * 3)
や string.substring(1, length-1)
など) 可能性があるので、その内側の式にも変換を施したものを返すための記述です。transformExpression
は、第 1 引数に AST 変換の処理が書かれたオブジェクトを渡すと、内側の式に対してその AST 変換を施します。
AST 変換の処理の実装 III: 式の変換
では、実際に処理を書いていきます。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// 左側の AST に対して再帰的に変換を行う
Expression nextReceiver = transform(left)
// 右側の AST だけからなる引数リストを作成して再帰的に変換を行う
Expression nextArguments = transform(new ArgumentListExpression(right))
// intdiv メソッド呼び出しの AST を作成する
Expression nextExpression = new MethodCallExpression(nextReceiver, "intdiv", nextArguments)
return nextExpression
}
}
return expression.transformExpression(this)
} else {
return null
}
}
基本的には、まず instanceof
演算子で何を表現している AST なのかを判別して、情報を取得していくという流れになります。
作成した AST 変換の利用
では、実際に今作った AST 変換を使ってみましょう。
ここまでで作ったソースコード (Intdiv.groovy
, IntdivTransformation.groovy
) を Groovy でコンパイルします。出力されたクラスファイルがクラスパスに含まれている状態で、以下のコードを実行してみましょう。
import ziphil.transform.Intdiv
@Intdiv
class Test {
public static void main(String... args) {
println(7 / 2)
}
}
3 が表示されれば成功です。@Intdiv
を取り除くと Groovy の仕様通り 3.5 になります。
問題点
ここで実装した AST 変換ですが、必要最低限の処理しかしていないので問題点がいくつかあります。
まず、今のままでは単純に /
を intdiv
に置き換えているだけなので、例えば 3.4 / 2
なども 3.4.intdiv(2)
に置き換えてしまい UnsupportedOperationException
が発生します。これを防ぐには、/
の両側の式の型を調べて、それが int
や long
などの場合のみに変換を施すようにするなどの工夫が必要です。
さらに、変換の際に新たに MethodCallExpression
を生成しているので、その式がソースコードのどの位置にあったかなどの情報が失われてしまっています。そのため、エラーが起こった際にスタックトレースに行番号などが表示されないなどの不具合が発生する可能性があります。これは、setSourcePosition
メソッドなどを用いてソースコード上の位置情報をセットし直せば解決できます。
AST 変換を作る際の注意点など
ここでは AST 変換自体も Groovy で実装しましたが、最終的にコンパイルされたクラスファイルがあれば良いので、Java で書いても問題ありません。実際、Groovy が標準でサポートしている AST 変換は Java で書かれています。
また、AST 変換を利用する場合は、AST 変換の処理が書かれたソースコードがすでにクラスファイルにコンパイルされている必要があります。したがって、AST 変換を実装するコードとその AST 変換を利用しているコードを同時にコンパイル (もしくは実行) しようとしてもうまくいきません。
このシリーズ
- Groovy で AST 変換 I: AST 変換を作ってみよう
- Groovy で AST 変換 II: GroovyConsole の使い方
- Groovy で AST 変換 III: Tips など
-
Groovy は標準機能としてゲッターとセッターを自動生成しますが、動的メソッド生成と AST 変換の違いを説明する例として挙げているだけなので、ここでは忘れてください。 ↩