Groovy で C# の partial class を実現する

  • 5
    いいね
  • 0
    コメント

初めに

これは G* Advent Calendar 2016 の 14 日目の記事です。

最近は AST 変換にハマっています。ただ、AST 変換はドキュメントが少ないのが難点です。ネットで検索しても、メソッド呼び出しを 1 行追加するなどの簡単なものしか見つからない上に、Groovydoc を見てもメソッドの説明があまりないので、複雑なものを作ろうとすると非常に苦労します。ということで、AST 変換を作ろうとしている人の助けに少しでもなるように、自分がやったことを公開していこうと思います。

AST 変換について

Groovy には AST 変換という機能があります。簡単に言うと、Groovy コードがバイトコードにコンパイルされる中間の段階で、プログラムの内容を書き換えてしまえる機能です。

これによって、いわゆるボイラープレートコードを削減したり、Groovy の構文をある意味で拡張して DSL を作ったりできるなど、非常に強力です。一方で、ソースコード上の処理と実際の処理が大きく変わってしまうため、デバッグがしにくくなったり IDE の機能が使えなくなったりします。使い方しだいで良い方向にも悪い方向にも働きますが、強力な機能であることには間違いありません。

partial class

C# には partial class という機能があり、1 つのクラスの定義を複数の箇所に分割して書けるようになっています。例えば、コードの一部を自動生成している場合に、自動生成部分と人間が書く部分とでファイルを分けておくことで、自動生成する部分が変化しても、人間が書く方のファイルを再作成する必要がなくなります。WPF において、XAML やデザイナによって生成されるフィールドなどが別ファイルになっているのがこれの一例ですね。

さて、この便利な partial class ですが、Java にはありませんし、Groovy にもありません。しかし、Groovy なら AST 変換を使って似たようなことができるので、実際にやってみました。

ソースコードは Gist に上げてあるので、そちらで見てください。

使い方

以下では、上のソースコードを Groovy でコンパイルして、できた class ファイルにパスが通った状態で実行されているものとします。

あるクラス A@Partial アノテーションを付加し、その引数に Class オブジェクト B を渡すと、A に定義されたフィールドやメソッドが B に自動的に移動されます。これによって、AB で 1 つのクラスを定義しているかのようなコーディングができます。

例えば、以下のようなコードを考えてみます。

public class Foo {
  private int field = 6
  public void test() {
    println(field)
    println(partialField)
  }
}

@Partial(Foo)  // これによって PartialFoo の中身が Foo に移動する
public class PartialFoo {
  private int partialField = 9
  public void partialTest() {
    println(field)
    println(partialField)
  }
}

これは以下のように書いたのと同じになります。したがって、例えば new Foo().test() を実行すれば、エラーが発生することなく「6」と「9」が表示されます。

public class Foo {
  private int field = 6
  public void test() {
    println(field)
    println(partialField)
  }
  private int partialField = 9
  public void partialTest() {
    println(field)
    println(partialField)
  }
}

@Partial(Foo)
public class PartialFoo {
}

@Partial アノテーションを付加するクラスの名前は何でも構いません。また、中身が移動するだけなので、アクセス修飾子などは移動先のクラスに合わせられます。また、コンパイルエラーを防ぐため、@Partial アノテーションが付加されたクラス内のフィールドやメソッドは全て削除されます。

@Partial の引数に設定するクラスは内部クラスであっても構いません。外部クラスの private フィールドもしっかりアクセスできます。例えば、以下のコードを書いた上で new Foo("World").test() を実行すると「Hello, World!」と表示されます。

public class Foo {
  private String name = ""
  public Foo(String name) {
    this.name = name
  }
  public void test() {
    Inner inner = new Inner()
    inner.hello()
  }
  private class Inner {  // ここに移動される
  }
}

@Partial(Foo.Inner)
public class PartialInner {
  public void hello() {
    println("Hello, ${name}!")  // Inner の外部クラス Foo のフィールドにアクセス可能
  }
}

部分的な解説

FieldTransformer について

ソースコード上の変数の出現は、AST では Variable オブジェクトとして管理されています。このクラスは accessedVariable というプロパティをもっていて、この値は同じ変数であれば (つまり同じ名前で同じスコープ上にあれば) 同一オブジェクトになっています。

AST が構成された時点で変数の定義が見つからなかった場合は、動的変数と見なされて accessedVariable の値は DynamicVarible 型になります。変数の定義がフィールドやローカル変数に見つかった場合は、accessedVarible の値はその変数 (フィールドなら FieldNode 型, ローカル変数なら VariableExpression 型) にセットされます。

さて、AST 変換を施す前の以下のソースコードを考えます。

@Partial(Foo)
public class PartialFoo {
  private int partialField = 9
  public void partialTest() {
    println(field)
    println(partialField)
  }
}

この段階では field という変数はどこにも定義されていないので、println(field) における field は動的変数だと解釈されます。したがって、partialTest メソッドをそのまま Foo クラスに移しただけでは、動的変数だと解釈されたままで、Foo のフィールド field を指しているとは解釈されず、実行すると変数が定義されていないと言われてエラーになります。したがって、accessedVariable の値を書き換える必要があります。それを行っているのが FieldTransformer クラスです。

FieldTransformer クラスは、コントラスタの第 1 引数に ClassNode オブジェクトを受け取ります。そして、visitMethodvisitConstructor メソッドによって式の走査が始まると、初めに受け取った ClassNode に定義されたフィールドと同じ名前の変数が現れたら、その変数の accessedVariable をそのフィールドに書き換えます (31 行目~ 39 行目)。さらに、もし ClassNode オブジェクトが内部クラスであったら、外部クラスのフィールドも調べます (40 行目~ 41 行目)。

追記: いろいろ調べてみると、変数のスコープを良い感じに直してくれる VariableScopeVisitor というユーティリティクラスがすでに Groovy にありました。諸々の処理の後に以下のコードを実行すると、accessedVariable の値などを書き換えてくれるようです。

VariableScopeVisitor visitor = new VariableScopeVisitor(sourceUnit)
visitor.visitClass(targetClass.redirect())  // visitClass や visitMethod などを呼ぶ

ただし、redirect メソッドは忘れないように。

PartialTransformation 内の redirect について

ClassNode オブジェクトには、実際にあるクラスを表現しているものと、別の ClassNode オブジェクトへのリンクになっているだけのものがあるようです。後者のような ClassNode オブジェクトに対しては redirect メソッドを呼ぶことで、リンク先の実際にクラスを表現している ClassNode オブジェクトを取得することができます1

ここで注意すべきなのは、リンクになっている ClassNode に対して addMethod メソッドなどでメソッドを追加してもうまくいかないことです。76 行目94 行目redirect メソッドを呼んでいるのはそのためです。

まとめ

ということで、Groovy でも partial class (のようなもの) が使えるようになりました。AST 変換は偉大ですね。

ただ最低限の実装しかしていないので、例えば @Partial アノテーションが付加された方のクラスに内部クラスがあったりしても、それは移動されません。また、移動先が同じ @Partial アノテーションが複数ある場合は、変数の参照などがうまくいかない可能性があります。この辺りはまた機会があったときに修正しようと思います。


  1. なぜこのような構造になっているのかはよく分かっていません。誰か教えてください・・・。