LoginSignup
0
0

More than 5 years have passed since last update.

Groovy で配列も newify

Posted at

今回実装する 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 をつけることで、以下のように書けるようになります。記述量は減ってませんが、newLeaf などの間にスペースがなくなるので見やすくはなっていると思います (個人差はありそうですが)。

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

実装

アノテーションの作成

いたって普通です。

NewifyArrays.groovy
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 を取得する必要が出てくるので、それを行えるユーティリティメソッドを用意しておきます。

TypeConverter.groovy
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 というユーテリティクラスが利用できます。このクラスの詳しい動作については、すでにこちらで解説してあるので、ここでは割愛します。

NewifyArraysTransformation.groovy
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): レシーバとメソッド本体と引数をそれぞれ取得します。レシーバを取得するメソッドは getObjectExpressiongetReceiver と 2 種類あるのですが、違いがよく分かりませんでした。

(3): (2) で取得した変数が想定している型かどうかを判定します。変換したい式は int[].new(4) の形式なので、レシーバがクラスを表す式 (ClassExpression) かどうか、またメソッド本体が定数 (ConstantExpression) であるかどうかを判定しています。引数は必ず ArgumentListExpression な気がするので判定が必要かどうか分かりませんが、キャストの意味も込めて判定しています。

(4): メソッド名が「new」かどうかを判定します。getText メソッドでメソッド名を String として取得できます。

(5): レシーバがクラスを表す式であることはすでに判定しましたが、今回実装する AST 変換はレシーバが配列クラスを表す式である場合に行うので、配列クラスかどうかを判定しています。ClassNodeisArray というそのままのメソッドがあるので、それを使っています。

(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] なら 35 の部分です。これを省略すると長さ 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 のリテラルはある) はずですが、なぜこんなものが用意されているのでしょうか・・・。


  1. 実用性はほぼないと思いますが、デフォルトの @Newify では配列でエラーになるのが気になったので、自分で修正版を作ってみたかったという動機がありました。 

  2. 作った後になって new int[][].new(2, 3) ではなくて int[2][3].new() でも良かったかなとも思ったのですが、遅かったです・・・。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0