uehaj's blog

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

Groovyクロージャはいかにしてクロージャであるか?

前回記事「Groovyのクロージャはちゃんとしているか?の続きです。

前回検討したGroovyのクロージャの振る舞いは、実際にJavaバイトコードとしてどのように実現されているでしょうか、ってのが今回のテーマです。

本来ならここで、

  • groovycコマンドでコンパイル
  • 生成された.classファイルをjadなどで逆コンパイル

という「黄金の二連コンボ」で生成される等価なJavaコードを示せばそれで済む話…だったはずなのですが、残念ながら、クロージャの使用コードから生成されるコードについてはjadは正しいコードを逆生成してくれません。バイトコード命令が延々と出てくるのはまだ良いとして、ローカル変数の型推論に失敗して嘘コードを吐きやがります*1

というのが、前回の記事を書いた経緯なのです。つまり、うまくgroovyc & jadできなかったので、実験して動作から推定するしかなかったのです*2。本記事ではjadの生成結果とも合せて検討した結果を紹介します。推定なので本当にこうである保証は無いのっす。

クロージャの実装推定

ということで、以下のようなクロージャを用いたGroovyコード:

class Test {
    def methodA() {
        int localVariable = 0
        return {-> localVariable++ }
    }
}

これは以下のようなJavaコードが生成するであろうバイトコードと同等のバイトコードにコンパイルされるんじゃないかと思います。あくまで推定です。本論に関わらないコードはバサッと省略しています。

import groovy.lang.Reference;
import groovy.lang.GroovyObject;
import groovy.lang.Closure;

public class Test implements GroovyObject {

    public Object methodA() {

        Reference<Integer> localVariable = new Reference<Integer>(new Integer(0));

        return new _methodA_closure1(localVariable) {
    }

    class  _methodA_closure1 extends Closure {
            Reference<Integer> localVariable;
            public Object doCall() {
                Object obj = localVariable.get();
                localVariable.set(localVariable.get().next())
                return obj;
            }

            public Integer getLocalVariable() {
                return localVariable.get()
            }

            public _methodA_closure1(Reference<Integer> localVariable) {
                this.localVariable = localVariable
            }
        }
}

ここで、groovy.lang.Referenceは、

public class Reference<T> extends GroovyObjectSupport implements Serializable {

    private T value;

    public Reference(T value) {
        this.value = value;
    }
 :
    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
}

こういう単一要素のコレクションでgroovy.langパッケージにあります。間接参照を表します。

クロージャの動作

言葉でまとめますと、

  • プリミティブのローカル変数は、ラッパー型にboxingされた上に*3、参照Referenceを介したものとして定義される(localVariable変数は型が変更される)。
  • クロージャ定義(.. {-> localVariable++ })は、Closureを継承した内部クラス(「クロージャクラス」と呼ぶ)をnewするコードに変換される。
  • Reference化されたローカル変数は、前述のクロージャクラスのコンストラクタにパラメータとして渡され、インスタンス変数として保持される(参照がコピーされる)。
  • クロージャ中のコードは、前述のクロージャクラスでOverride定義されたdoCallメソッドのコードに展開される。
  • doCall中からのローカル変数の参照や変更は、クロージャクラスがインスタンス変数として保持するReferenceオブジェクトを通じて行われる。

いかがでしょう。予想どおりだったでしょうか。

クロージャであるために

この実装では、前回の記事の「クロージャが満すべき条件」はすべて解決されているのではないかと思います。

ポイントは、

(1)ローカル変数の値の保持領域をスタックからヒープに移動させる(プリミティブ型はラッパーになる)
→ リターンしてもクロージャから参照される変数が生存している
(2)複数クロージャからの「変数の共有」を実現するために、「変更可能な間接参照」を表現するオブジェクト(groovy.lang.Reference)」を介在させる。
→ 複数クロージャ間で書き換え可能な変数の共有が可能になる
(3)クロージャは出現するたびに毎回newする(再利用しない)。そしてローカル変数を保持する参照は、クロージャインスタンスに保持させる
再帰呼び出しを可能に

です。

ちなみに、Javaでも内部クラスからfinal変数を変更するために、int型(final int a=1)を1要素のint配列(final int[] a={1})にしたりしますが、上は本質的に言ってそれと同様です。int配列を使う場合には、(1)ヒープへの移動と、(2)書き換え可能な間接参照は、書き換え可能な参照型である配列が実現します。また、int配列を(3)コピーすることは、内部クラスの仕様として、裏でjavacがやってくれています。

では、次に気が向いたら「クロージャとは」的な議論を書きます。

*1:とはいえjadに文句を言ってもしょうがない。javacはもともと可逆変換である保証は無いし、いわんやgroovycの出力においてをや。

*2:Groovyのバイトコード生成コードを読むという手もあったけど、嫌だった。

*3:もとがプリミティブ型ではなく参照型であれば、boxingされずそのままReferenceにラッピングされるのではないかと。