uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

@Fieldかわいいよ@Field

はじめに

息切れ直前企画「プログラミングGroovyの発売日まで1日1エントリ書くぞう」の第4回目です。

今いちネタがないので、先日JGGUG G*WorkshopでGroovy 1.8の新機能について紹介した際の補足として、新規AST変換の1つ@Fieldを解説してみたいと思います。

前ふり: AST変換の分類

Groovy 1.8では「AST変換」が特に強化された機能だと思います。とても多くのAST変換が追加されています。発表では、これらを以下の3つのカテゴリに便宜的にわけてみました*1

  • ボイラープレート割り
  • 巨人の肩
  • 言語機能の補修、拡張

「ボイラープレート割り」は、冗長なコードを排して、簡潔にするためのものです。@EqualsAndHashCodeとか@Logとかですね。これは「無くても良いけど知ってると便利」なもの。

「巨人の肩」は、デバッグ済みのコーディングパターンを再利用するためのものです。「再利用」のライブラリ以外の形態です。@Immutableとか、@WithReadLock+@WithWriteLockがあてはまるのではないかと思います。「知ってるととても便利」なもの。用途にぴったりあえば、コード量や品質について圧倒的な差となるかもしれないもの。

そして3番目のカテゴリ「言語機能の補修、拡張」には、@ThreadInterrupt、@Fieldなどがあてはまるのではないかと思っています。これらは「知らないと損」なものです。

そして、3番目のカテゴリでは、「@Field」が特に有用だと思います。私見ですが、これが使える、ということだけでもGroovy 1.8に移行する理由になると思います。@FieldについてはG*WSでは説明しなかったので、本エントリで紹介してみます*2

なお、@Fieldは言語機能の不備を解決するものだと思っているのですが、「どんな不備があったのか」を説明する必要があるので、本記事はちょっと長くなってしまっています。この問題に直面したことがある人であれば、ひとことで済む話ではあるのですが。

@Field AST変換とは何か

@Fieldは、スクリプト中で変数宣言に指定することで、その変数がスクリプトのクラスのフィールドとなるような働きを持つAST変換です。利用イメージはこんな感じ。

import groovy.transform.Field
@Field int i = 3;

しかし、@Fieldの意義を理解するには、前提としてGroovyスクリプトがどのようにJava Byte Codeのクラスにコンパイルされるかを知っておく必要があります(知ってる人はこの節は飛ばしてください)。Groovyにおけるスクリプトは、見た目上、クラスに所属しないコードです。たとえば以下がスクリプトの例です。

println "hello"

しかし、このようなスクリプトも、Groovyの実行の仕組み上、暗黙に生成されるクラスのメソッドに変換された上でJava Bytecodeとして実行されます。例えば、上が「hello.groovy」というファイルに保存されていたとしたら、

public class hello extends Script {
 public void run() {
   System.out.println("hello");
 }
 public static void main() {
   new hello().run();
 }
}

というようなクラスにコンパイルされて実行されます(細部はかなり省略しています)。

ローカル変数とバインディング変数

さて、スクリプト中の変数宣言には2通りあって、1番目は

def a = 0
int i =1
String s = "ABC"

というような変数の型宣言をともなうもの(ローカル変数)です。二番目は、

b = 1
j = 1
t = "DEF"

のようないきなり代入するような使い型です。後者はスクリプト特有のもので「バインディング変数」とよばれるものです。バインディング変数はすべてObject型あつかいで、型指定ができません。

ローカル変数は、その名のとおり、メソッドのローカル変数を意味します。どのメソッドかというと、前節で説明した暗黙に生成されている「runメソッド」のローカル変数です。以下のようなスクリプトは、

int i=1
println i

以下のようなクラス定義から生成されるコードと同様なコードに生成されて実行されます。

class hello extends Script {
  public void run() {
    int i = 1;
    System.out.println(i);
  }
}

ローカル変数とバインディング変数の得失

ローカル変数とバインディング変数はスクリプト中で明確に区別して使いわける必要があります。

ローカル変数 バインディング変数
実体 スクリプトがコンパイルされたクラスのrunメソッドのローカル変数 Binding(ハッシュ)のようなもの
利用可能場所 メソッド中などどこでも スクリプト中、もしくはスクリプト中で定義されたメソッド中のみ
型指定 可能 不可能(Object型のみ)
他メソッドからの利用 不可

何が問題か

さて問題は、前節「他メソッドからの利用」の最後の項目でも示したように、ローカル変数はメソッド、特にスクリプト中で定義したメソッドからも参照できないということです

int i = 1;
def foo() {
  println i
}
foo()

これはprintln iで「iが参照できない」というエラーになります。なぜエラーかは、このスクリプトがどんなコードに生成されるかを想像すればわかるのですが、

class hello extends Script {
  public void run() {
    int i = 1;
    foo()
  }
  public Object foo() {
    System.out.println(i); // runのiは参照できない
  }
}

こうなるからです。要はメソッドfoo中からは別のメソッドであるrun()のローカル変数iを参照できないということです。

これが問題です。

@Fieldの出番だ

はてさて、ならば、すべてをバインディング変数にすれば良いという意見もありましょう*3

ただ、バインディング変数だと、

  • 代入することで存在を開始するので、その変数のコード上の唯一の「定義場所」が存在しなくなる。変数にコメントをつけるとき、どこにコメントをつければ良いのか。
  • 型宣言が付与できない。オプショナルタイピングができない。

要は「Rubyみたいで嫌なんです」ああ言ってしまった〜*4

で、この問題は、@Fieldが解決します。

import groovy.transform.Field
@Field int i;
def foo() {
  println i
}
foo()

とすると、このコードは

class hello extends Script {
  int i = 1;  // フィールドになる
  public void run() {
    foo()
  }
  public Object foo() {
    System.out.println(i);
  }
}

というコードに生成されるので、問題なく実行できます。
iはローカル変数ではなく、クラスのフィールドになるのです。@Fieldかわいいよ@Field*5

@Fieldがあれば、バインディング変数はいらないんじゃないかと思いますね。スクリプトを外部から呼び出す場合に、設定として値を渡すときなどは便利なので、無くても良いとはいえないかなと思い直しました。

おまけ

@Fieldは、以下のようにクロージャ中で変数宣言に指定すると、やはりスクリプトクラスのフィールドにできます。

import groovy.transform.*
c={
  @Field j=3
  println j
}
assert j == 3  // cを呼んでないのに!

これに意味がある使いかたがあるかどうかは不明です。

*1:かならずしも直和分割されるものではなく、重複してあてはまるものもあるのではないかと思います。

*2:[http://www.amazon.co.jp/exec/obidos/ASIN/4774147273/uehaj-22/ref=nosim/:title=プログラミングGroovy]では解説しています

*3:あるいはクロージャからはローカル変数が参照できるので、すべてをクロージャにする、という案もあったかもしれません。実際、Groovy 1.8以前ではそういう判断をしていたこともあるかもしれませんが、いずれも苦肉の策であり、今考えるといずれも不備のある言語仕様に対するバッドノウハウということになります。

*4:もちろんこれは主観です。Rubyっぽくないので嫌いです、という意見もあることでしょう。

*5:とはいえ、全体として決してわかりやすい仕様ではありませんね。「Groovyのここが嫌」で列挙されそうなところかもしれません。