今回実装する AST 変換
Groovy が標準で提供している AST 変換に @Newify
というのがあるのですが、ご存知でしょうか。Groovy では新しいオブジェクトを生成するときに new SomeClass()
と書くのですが、これを Ruby っぽく SomeClass.new()
と書いたり、Python っぽく SomeClass()
と書いたりできるようにする AST 変換です。
これに何の意味があるかというと、コンストラクタをたくさん呼ぶ必要があるときに、見た目を良くしたり記述を減らしたりすることができます。Groovydoc にもある例ですが、木構造をオブジェクトで表現するときに、AST 変換なしでは以下のように書くことになります。
new Tree(new Leaf("A"), new Tree(new Tree(new Leaf("B"), new Leaf("C")), new Leaf("D"))
@Newify
をつけることで、以下のように書けるようになります。記述量は減ってませんが、new
と Leaf
などの間にスペースがなくなるので見やすくはなっていると思います (個人差はありそうですが)。
Tree.new(Leaf.new("A"), Tree.new(Tree.new(Leaf.new("B"), Leaf.new("C")), Leaf.new("D"))
もしくは以下のようになります。new
がない分だけ記述が簡潔になり、さらに見やすいですね。
Tree(Leaf("A"), Tree(Tree(Leaf("B"), Leaf("C")), Leaf("D"))
さて、この @Newify
ですが、配列に関してはうまくいきません。new int[3][4]
のつもりで int[][].new(3, 4)
などと書いても new int[][](3, 4)
に変換されてしまい、エラーになります。 そこで、これをちゃんと new int[3][4]
に変換する AST 変換を作ってみます。実用的かは置いておいて、とにかく AST 変換のサンプルにでもなれば幸いです12。
実装
アノテーションの作成
いたって普通です。
package ziphil.transform
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.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD])
@GroovyASTTransformationClass(["ziphil.transform.NewifyArraysTransformation"])
public @interface NewifyArrays {
}
アノテーションは、メソッド (コンストラクタ含む) とクラスとフィールドに付加できるようにしておきます。
配列の要素の型を取得するメソッドの作成
AST 変換を実装するときに、配列型を表す ClassNode
からその要素の型を表す ClassNode
を取得する必要が出てくるので、それを行えるユーティリティメソッドを用意しておきます。
package ziphil.transform
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassNode
@CompileStatic
public class TypeConverter {
public static ClassNode toComponentType(ClassNode type) {
ClassNode componentType = type
while (componentType.isArray()) {
componentType = componentType.getComponentType()
}
return componentType
}
}
ClassNode
クラスの getComponentType
メソッドは、配列型の ClassNode
に対してその要素の型を返します。ただし、多次元配列の場合は次元が 1 つ下がった型が返ってくるだけ (String[][]
の ClassNode
に対して getComponentType
を呼ぶと String[]
の ClassNode
が返る) なので、それを繰り返し呼んでいます。
AST 変換の処理の実装
式の変換を行う AST 変換なので、ClassCodeExpressionTransformer
というユーテリティクラスが利用できます。このクラスの詳しい動作については、すでにこちらで解説してあるので、ここでは割愛します。
package ziphil.transform
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ArrayExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.ConstructorCallExpression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.MethodCallExpression
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.CANONICALIZATION)
@CompileStatic
public class NewifyArraysTransformation extends ClassCodeExpressionTransformer implements ASTTransformation {
private SourceUnit sourceUnit
public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
AnnotatedNode parent = (AnnotatedNode)nodes[1]
this.sourceUnit = sourceUnit
if (parent instanceof ClassNode) {
visitClass(parent)
} else if (parent instanceof MethodNode) {
visitMethod(parent)
} else if (parent instanceof FieldNode) {
visitField(parent)
}
}
public Expression transform(Expression expression) {
if (expression != null) {
// (1) メソッド呼び出しの場合
if (expression instanceof MethodCallExpression) {
// (2) レシーバとメソッド本体と引数をそれぞれ取得
Expression receiver = expression.getObjectExpression()
Expression method = expression.getMethod()
Expression arguments = expression.getArguments()
// (3) クラスを判定
if (receiver instanceof ClassExpression && method instanceof ConstantExpression && arguments instanceof ArgumentListExpression) {
// (4) メソッド名が new の場合
if (method.getText() == "new") {
// (5) レシーバの型が配列の場合
if (receiver.getType().isArray()) {
// (6) 配列の要素の型を取得
ClassNode nextType = TypeConverter.toComponentType(receiver.getType())
Expression nextArguments = transform(arguments)
// (7) 配列の初期化を表す AST を生成
Expression nextExpression = new ArrayExpression(nextType, null, ((ArgumentListExpression)nextArguments).getExpressions())
// (8) ソースコード上の位置を設定
nextExpression.setSourcePosition(expression)
return nextExpression
}
}
}
// (9)
} else if (expression instanceof ClosureExpression) {
expression.getCode().visit(this)
// (10)
} else if (expression instanceof ConstructorCallExpression) {
if (expression.isUsingAnonymousInnerClass()) {
expression.getType().visitContents(this)
}
}
return expression.transformExpression(this)
} else {
return null
}
}
public SourceUnit getSourceUnit() {
return this.sourceUnit
}
}
ソースコードを順番に解説していきます。
(1): メソッド呼び出しの AST に対応する型は MethodCallExpression
です。変換したい式は int[].new(4)
のようなメソッド呼び出しの形なので、まず expression
がメソッド呼び出しの AST かどうかを判定します。
(2): レシーバとメソッド本体と引数をそれぞれ取得します。レシーバを取得するメソッドは getObjectExpression
と getReceiver
と 2 種類あるのですが、違いがよく分かりませんでした。
(3): (2) で取得した変数が想定している型かどうかを判定します。変換したい式は int[].new(4)
の形式なので、レシーバがクラスを表す式 (ClassExpression
) かどうか、またメソッド本体が定数 (ConstantExpression
) であるかどうかを判定しています。引数は必ず ArgumentListExpression
な気がするので判定が必要かどうか分かりませんが、キャストの意味も込めて判定しています。
(4): メソッド名が「new」かどうかを判定します。getText
メソッドでメソッド名を String
として取得できます。
(5): レシーバがクラスを表す式であることはすでに判定しましたが、今回実装する AST 変換はレシーバが配列クラスを表す式である場合に行うので、配列クラスかどうかを判定しています。ClassNode
に isArray
というそのままのメソッドがあるので、それを使っています。
(6): 上で作ったメソッドを使い、配列の要素の型を取得します。
(7): 変換後の AST を生成します。配列の生成の AST に対応する型は ArrayExpression
なので、そのコンストラクタを呼んでいます。ArrayExpression
のコンストラクタの引数については、後で詳しく説明します。
(8): nextExpression
はこの場で生成した AST なので、ソースコード上の位置と結びついていません。これを設定するには lineNumber
, columnNumber
, lastLineNumber
, lastColumnNumber
という 4 つのプロパティを設定する必要がありますが、すでにある式と全く同じ値を設定するだけなら、setSourcePosition
という便利なメソッドがあります。ここでは、もともとの式である expression
と同じ値を設定すれば良いので、このメソッドを利用しています。
(9), (10): ClassCodeExpressionTransformer
クラスは、visit~
メソッドを呼ぶとそれに含まれる全ての式を引数にして transform
メソッドを呼んでくれるのですが、クロージャや匿名クラスの中身については transform
メソッドを呼んでくれません。そこで、この箇所で自前で呼んでいます。
実際に使ってみる
上で作成した 3 つのファイル (NewifyArrays.groovy
, NewifyArraysTransformation.groovy
, TypeConverter.groovy
) を Groovy でコンパイルし、それによってできたクラスファイルがクラスパスにある状態で以下のコードを実行してみましょう。
import ziphil.transform.NewifyArrays
public class Test {
@NewifyArrays static int[] field = int[].new(10)
@NewifyArrays
public static void main(java.lang.String[] args) {
String[][] array = String[][].new(2, 3)
array[0][0] = "foo"
array[1][2] = "bar"
field[3] = 3
field[4] = 4
println(array)
println(field)
}
}
以下のように出力されれば成功ですね。
[[foo, null, null], [null, null, bar]]
[0, 0, 0, 3, 4, 0, 0, 0, 0, 0]
ArrayExpression
のコンストラクタについて
ArrayExpression
の Groovydoc を見ると、第 3 引数を省略するかしないかでコンストラクタが 2 種類あります。
第 1 引数の elementType
は配列の要素の型です。注意すべきなのは、String[]
型の配列を生成したいなら elementType
には String
を表す ClassNode
を渡さなければならない点です。渡す型は配列型ではなく配列の要素の型です。
第 3 引数の sizeExpression
では配列のサイズを表す式を格納した配列を指定します。new int[3][5]
なら 3
と 5
の部分です。これを省略すると長さ 0 の 1 次元配列になります。以下のコードは new int[3][5]
の AST を生成します。
ClassNode type = ClassHelper.make("int")
Expression firstSize = new ConstantExpression(3)
Expression secondSize = new ConstantExpression(5)
Expression expression = new ArrayExpression(type, null, (List<Expression>)[firstSize, secondSize])
問題は第 2 引数です。いろいろ試行錯誤した結果、どうやら指定した要素を格納した状態の配列を返すようです。例えば、以下のコードでは "FIRST"
と "SECOND"
が格納された長さ 2 の String[]
オブジェクトの AST が生成されます。Java なら new String[]{"FIRST", "SECOND"}
に対応するものです (この記法は Groovy でサポートされていません)。
ClassNode type = ClassHelper.make("java.lang.String")
Expression first = new ConstantExpression("FIRST")
Expression second = new ConstantExpression("SECOND")
Expression expression = new ArrayExpression(type, (List<Expression>)[first, second])
ちなみに、第 2 引数と第 3 引数が両方指定されている場合、第 2 引数は無視されるようです。以下のコードでは各要素が null で初期化された長さ 3 の String[]
オブジェクトの AST が生成されます。つまり、第 2 引数に null を指定したのと同じ結果になります。
ClassNode type = ClassHelper.make("java.lang.String")
Expression first = new ConstantExpression("FIRST")
Expression second = new ConstantExpression("SECOND")
Expression size = new ConstantExpression(3)
Expression expression = new ArrayExpression(type, (List<Expression>)[first, second], (List<Expression>)[size])
Groovy には配列のリテラルはない (List のリテラルはある) はずですが、なぜこんなものが用意されているのでしょうか・・・。