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

Groovy で扱える既存の数値データBigDecimal、BigInteger、Byte、Double、Float、Integer、Long、および Shortに加え、複素数も Groovy の計算式の構文の中で扱えるよう拡張してみます。

Groovy では様々なデータの上で使用可能な演算子のオーバーローディングが用意されていて、Groovy の演算子メソッドの呼び出しに対応しています。例えば、a+b であれば a.plus(b) に対応しています。以下に四則演算の例を示します。

演算子 メソッド
a+b a.plus(b)
a-b a.minus(b)
a*b a.multiply(b)
a/b a.div(b)

(http://groovy-lang.org/operators.html#Operator-Overloading)

つまり、オブジェクト a のクラスにおいて、演算子に対応するメソッドを用意しておけばよいのです。

このおかげで、新たなデータ型を用いた数式を Groovy の構文の中で簡単に表現することが可能となります。
もしも演算子のオーバーローディングがない場合には、新規に数式の構文を自前で定義しそしてパーサを実装する必要があります。まぁ、それはそれで楽しい体験だとは思いますがなかなか大変な作業です。

複素数演算子のオーバーローディングに入る前に、まず複素数を表現するクラスを示します。

//Complex.groovy より抜粋1:
public class Complex {
  // フィールド
  private Number r = 0 // 実数部
  private Number i = 0 // 虚数部
  public Number getR() {r} // 実数部の参照メソッド
  public Number getI() {i} // 虚数部の参照メソッド

  // コンストラクタ
  public Complex (Number r, Number i) { // 実数部+虚数部からなる複素数
    this.r=r
    this.i=i
  }

  // 文字列化
  public String toString() {
    "${this.r} + ${this.i}i"
  }
  …

ここでは、実数部・虚数部のフィールド変数は Number クラスにしています。
既存の数値クラス(BigDecimal、BigInteger、Byte、Double、Float、Integer、Long、および Short)の値を使えるようにしています。
また、実数部・虚数部のフィールド変数はprivate 宣言をしておき、public 宣言した参照メソッド getR, getI を用意することで、外部からの書き込みを制限しつつ参照のみ許可するようなアクセス制御を行っています。

外部からの参照は a.getR() や a.getI() だけでなく、a.r や a.i のように記述できます。一見して直接フィールド変数にアクセスさせるような記述ですが、実際には getR() あるいは getI() メソッドが呼ばれます。

これらのフィールド変数の書き換えはコンストラクタからの初回の代入のみとします。
異なるフィールド変数の複素数オブジェクトが必要な場合には新たにオブジェクトを生成する(new する)以外の手段を与えないこととします。

複素数データ用の演算子のオーバーローディングはこんな感じです。

//Complex.groovy より抜粋2:
  // 加算(複素数+複素数)
  public Complex plus(Complex b) {
    new Complex(this.r+b.r, this.i+b.i)
  }
  // 乗算(複素数×複素数)
  public Complex multiply(Complex b) {
    new Complex((this.r*b.r)-(this.i*b.i), (this.r*b.i)+(this.i*b.r))
  }

複素数 a と複素数 b とをある演算にかけると新しい複素数が生まれます。
例えば加算の場合には次のような新しい複素数が生まれます。

  • その実数部に、a と b の実数部の和の値を持つ。
  • その虚数部に、a と b の虚数部の和の値を持つ。


続いて、複素数 a と数値 n との演算です。

//Complex.groovy より抜粋3:
  // 加算(複素数+数値)
  public Complex plus(Number n) {
    new Complex(this.r+n, this.i)
  }
  // 乗算(複素数×数値)
  public Complex multiply(Number n) {
    new Complex(this.r*n, this.i*n)
  }

もちろん、数値 n から実数部を n、虚数部を 0 とする複素数を作って、抜粋2のメソッドを呼ぶような実装も可能です。ただし、その場合にはオブジェクトの生成が一回多くなるのでやや無駄があります。

//参考:別の実装例
  // 加算(複素数+数値)
  public Complex plus(Number n) {
    this.plus(new Complex(n, 0))
  }
  // 乗算(複素数×数値)
  public Complex multiply(Number n) {
    this.multiply(new Complex(n, 0))
  }

ここまでで、複素数データを用いたスクリプトは次のように記述できます。

// test1.groovy
def i = new Complex(0, 1) // 虚数 i

def z1 = i + 2
def z2 = i * 5 + 1

println z1 * z2

z1 の右辺は次のように変換・評価されます。

z1 = i + 2 => i.plus(2) => new Complex(2, 1)

したがって、z1 は new Complex(2, 1) と同じ内容のオブジェクトが代入されます。

z2 の右辺を考えてみます。ここでは加算+と乗算×の二つが存在します。演算子の強さは Groovy で定められた順位で決まり、つまり、加算よりも乗算の演算子の方が強いので次のようになります。

z2 = i * 5 + 1 => (i.multiply(5)).plus(1) => (new Complex(0, 5)).plus(1) => new Complex(1, 5)

したがって、z2 は new Complex(1, 5) と同じ内容のオブジェクトが代入されます。

test1.groovy の実行結果です。

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

手で計算すると、

z1 * z2 = (2 + i)*(1 * 5i) = (2*1 - 5*1) + (2*5 + 1*1)i = -3 + 11i

となり、とりあえず処理結果は正しい値です。

ここまでは、一見うまくいってるように見えます。

課題1: 減算(-)や除算(÷)を実装して下さい。

「一見うまくいってるように見える」と書きました。test1.groovy では

 複素数+実数 あるいは 複素数×実数

の数式になっていました。加算と乗算は交換則がなりたつべきですし、逆の並びの

 実数+複素数 あるいは 実数×複素数

のようにも記述できてほしいです。

先ほどのコードを書き換えてみます。

// test1x.groovy
def i = new Complex(0, 1) // 虚数 i

def z1 = 2 + i // 実数+複素数に書き換え
def z2 = 5 * i + 1 // 実数×複素数に書き換え

println z1 * z2

これを実行すると、次のようなエラーメッセージが表示されます。

C:\work\>groovy test1x.groovy
Caught: groovy.lang.MissingMethodException: No signature of method: java.lang.Integer.plus() is applicable for argument types: (Complex) values: [0 + 1i]
Possible solutions: plus(java.lang.Number), plus(java.lang.String), plus(java.lang.Character), abs(), use([Ljava.lang.Object;), minus(java.lang.Character)
groovy.lang.MissingMethodException: No signature of method: java.lang.Integer.plus() is applicable for argument types: (Complex) values: [0 + 1i]
Possible solutions: plus(java.lang.Number), plus(java.lang.String), plus(java.lang.Character), abs(), use([Ljava.lang.Object;), minus(java.lang.Character)
        at test1x.run(test1x.groovy:4)

Integer クラスに Complex クラスのインスタンスを引数としてとるような plus メソッドがないよ!と怒られてしまいました。
当然といえば当然ですが、困りました。。。(>_<;)

実は、既存のクラスにインスタンスメソッドを追加する方法が Groovy にはあります。
それが metaClass です。例を見てみましょう。

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

def i = new Complex(0, 1) // 虚数 i
def z1 = 2 + i
def z2 = 1 + 5 * i
println z1 * z2

ここでは Number クラスに、複素数オブジェクト用の plus と multiply のメソッドクロージャを使って追加しています。キーワード "delegate" の部分には、これらのメソッドが適用されるインスタンスオブジェクトが入ります。

直接のエラーが Integer クラスで起きていますので Integer クラスで対処すべきだろうと思われるでしょうが、Number クラスで対処することで、その全サブクラスつまり、BigDecimal、BigInteger、Byte、Double、Float、Long、および Short の値で同じ効果が得られます。

z1, z2 の右辺は次のように変換されます。

z1 = 2 + i => 2.plus(i) => new Complex(2, 1)

z2 = 1 + 5 * i => 1.plus(5.multiply(i)) => 1.plus(new Complex(0, 5)) => new Complex (1, 5)

実行してみましょう。

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

test1.groovy と同じ結果となりました!

このようにして、metaClass を使うことで

実数+複素数 あるいは 実数×複素数

も行えるようになりました。

課題2: 課題1 に続き、残りの減算(-)や除算(÷)を完成させて下さい。

課題3: 複素共役(complex conjugation)を生成する単項演算子メソッドを作成して下さい。(ヒント:~a で、a.bitwiseNegate()あたりを使って下さい。)

(続く)