Groovy スクリプトで複素数データを扱う その3

次のようなスクリプトではうまくいくでしょうか。

// test4.groovy
class test4 {
  public static void main (String [] args) {
    def z1 = 2 + i
    def z2 = 1 + 5 * i
    println z1 * z2
  }
}

前回用意した mygroovy.bat で動かしてみましょう。

C:\work>mygroovy test4.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
C:\work\test4.groovy: 4: Apparent variable 'i' was found in a static scope but doesn't refer to a local variable, static field or class. Possible causes:
You attempted to reference a variable in the binding or an instance variable from a static context.
You misspelled a classname or statically imported field. Please check the spelling.
You attempted to use a method 'i' but left out brackets in a place not allowed by the grammar.
 @ line 4, column 18.
       def z1 = 2 + i
                    ^

C:\work\test4.groovy: 5: Apparent variable 'i' was found in a static scope but doesn't refer to a local variable, static field or class. Possible causes:
You attempted to reference a variable in the binding or an instance variable from a static context.
You misspelled a classname or statically imported field. Please check the spelling.
You attempted to use a method 'i' but left out brackets in a place not allowed by the grammar.
 @ line 5, column 22.
       def z2 = 1 + 5 * i
                        ^

2 errors

変数 i が見つからないと怒られてしまいました。

前回の GroovyShell クラスで evaluate するスクリプトがクラス定義の場合には Binding クラスで用意したビルトイン定数が有効にならないようです。

言い換えれば、前回の手法はクラス定義ではないスクリプト限定の手法だったのです。

今回のようなクラス定義の中では次のように記述する必要があります。

// test4x.groovy
class test4 {
  private static Complex i = new Complex(0, 1) // 虚数単位 i
  public static void main (String [] args) {
    def z1 = 2 + i
    def z2 = 1 + 5 * i
    println z1 * z2
  }
}

たった一行ですが。。。まぁ、煩わしいといえば、煩わしいですね。

これを省略してしまうには、AST 変換を使う必要があります。

AST 変換とは、スクリプトソースコードコンパイルした時に生成される抽象構文木 (Abastract Syntax Tree) を動的に変換することです。上のケースで言えば、虚数単位 i のフィール変数に関する次の一行を AST 変換でクラスに追加することができます。

  private static Complex i = new Complex(0, 1) // 虚数単位 i

以下では次の二つを行います。

(1) Complex.groovy の修正

Number クラスに複素数オブジェクト用の演算の追加を、前回は calc.groovy の中で行っていましたが、Complex クラスの中で行うよう変更します。

(2) ASTTransformation の実装

虚数単位 i のフィールド変数を追加する AST 変換を実装します。

(3) JAR ファイルの作成

AST 変換を行う JAR ファイルを作成します。

順番にみていきましょう。

(1) Complex.groovy の修正

//Complex.groovy より抜粋:
package bunji
public class Complex {

  // Number クラスに複素数オブジェクト用の演算を追加する。
  static {
    // 加算
    Number.metaClass.plus = {Complex b ->
      new Complex (delegate + b.r, b.i)
    }
    // 減算
    Number.metaClass.minus = {Complex b ->
      new Complex (delegate - b.r, -b.i)
    }
    // 乗算
    Number.metaClass.multiply = {Complex b ->
      new Complex (delegate * b.r, delegate * b.i)
    }
    // 除算
    Number.metaClass.div = {Complex b ->
      new Complex (delegate, 0)/b
    }
  }

…

このように記述しておくと、Complex が読み込まれた時にNumber クラスへの複素数オブジェクト用の演算が追加されまs。

(2) ASTTransformation の実装

package bunji
import org.codehaus.groovy.syntax.*
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.stmt.*
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*
import bunji.Complex

@GroovyASTTransformation(phase=CompilePhase.CONVERSION)
public class MyASTTransformation3 implements ASTTransformation {

  public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {

    def ast = sourceUnit.getAST()
    def cn = pickup_main_class(ast)
    if (cn != null) {
      add_field_i(cn)
    }
  }
  
  ClassNode pickup_main_class (ModuleNode mn) {
    String mainClassName = mn.mainClassName
    if (mn.classes != null) {
      for (ClassNode cn in mn.classes) {
        if (cn.name == mainClassName) {
          return cn
        }
      }
    }
    return null
  }

  void add_field_i (ClassNode cn) {
    def n0 = new ConstantExpression (0)
    def n1 = new ConstantExpression (1)
    def n2 = [n0,n1]
    def n3 = new ArgumentListExpression (n2)
    def n4 = new ConstructorCallExpression (
               ClassHelper.make(bunji.Complex,false),n3)
    def n5 = new FieldNode ("i",26,
               ClassHelper.make(bunji.Complex,false),cn,n4)
    cn.addField(n5)
  }

} // End of class

メインクラスにフィールド変数 i を追加します。

(3) AST 変換を行う JAR ファイル mycomplex.jar を作成します。

C:\work> groovyc -d out Complex.groovy
C:\work> groovyc -d out -cp out MyASTTransformation3.groovy
C:\work> mkdir out\META-INF\services
C:\work> echo bunji.MyASTTransformation3> out\META-INF\services\org.codehaus.groovy.transform.ASTTransformation
C:\work> jar cvf mycomplex.jar -C out bunji -C out META-INF

Complex クラスと MyASTTransformation3 クラスをアーカイブしています。

次のようにして実行します。

C:\work> groovy -cp mycomplex.jar;. test4.groovy
-3 + 11i

もちろん、前回の test3.groovy も動作します。

C:\work> groovy -cp mycomplex.jar;. test3.groovy
-3 + 11i

今回もバッチファイル化しておきましょう。バッチファイル名は mygroovy2.bat とでもします。

JAR ファイル mycomplex.jar は %GROOVY_HOME%\mylib の下にコピーしておくのを忘れずに。。。

@echo off
"%JAVA_HOME%\bin\java.exe" ^
 "-Xmx128m" ^
 -Dprogram.name="" ^
 -Dgroovy.home="%GROOVY_HOME%" ^
 -Dgroovy.starter.conf="%GROOVY_HOME%\conf\groovy-starter.conf" ^
 -Dscript.name="%1" ^
 -classpath "%GROOVY_HOME%\lib\groovy.jar" ^
 org.codehaus.groovy.tools.GroovyStarter ^
 --main groovy.ui.GroovyMain ^
 --conf "%GROOVY_HOME%\conf\groovy-starter.conf" ^
 --classpath "%GROOVY_HOME%\mylib\mycomplex.jar;." ^
 "%1"

実行は以下の通りです。

C:\work> mygroovy2 test3.groovy
-3 + 11i

(続くかも?)