やっとこさクロージャのスコープが分かって来たかも

Groovyイン・アクションの 5.5 スコープを理解する でうーんうーんと悩み続けてた今日この頃。
でも jad パワーで groovyc でコンパイルした class ファイルを逆コンパイルしたりして、やっとこさ分かってきた気がする。
分かってくると本に書いてある通りなんですが、全然理解出来なくて散々悩みまくった ( ;´-`)=3


そもそも悩み始めたきっかけは

int x = 0
10.times {
  this.x++
  println x
}
assert x == 10

これがどーして動くのかがさっぱりわからん、という所 (´・ω・`)
{} の中身はクロージャっつー事は、Closure 型のオブジェクトなわけで、そうなったらクロージャの内部で this 参照したらクロージャ自身だろうからクロージャの外にある x を this.x でみれるわけねーじゃーん、という感じ。


本を読んでも全然ピンとこなくて考えても良く分からなかったので、とりあえず class ファイルを逆コンパイルして Java のソースとして見ればなんかわかるんじゃね? という思いつきから jad 使ってさくっと逆コンパイルしてみた。
もちろん、たまたま今の groovyc がこういうバイトコード作ってるだけで将来的には構造が変わる事は大いにあり得るので、とりあえず現状を理解する為だけの感じで。
一部正常に逆コンパイルしきれなくて jad さんが悲鳴をあげたりしてますが、要点となりそうな箇所だけピックアップすると

import groovy.lang.*;
import java.io.File;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;

public class test2 extends Script
{
...
    public Object run()
    {
        Class class1 = test2.class;
        Class class2 = groovy.lang.MetaClass.class;
        Integer x = new Reference(new Integer(0));
        class _run_closure1 extends Closure
            implements GeneratedClosure
        {
            public Object doCall(Object it)
            {
                Class class3 = test2$_run_closure1.class;
                Class class4 = groovy.lang.MetaClass.class;
                Object obj = x.get();
                Object obj1 = ScriptBytecodeAdapter.invokeMethod0(test2.class, x.get(), "next");
                Object _tmp = obj1;
                x.set(obj1);
                Object _tmp1 = obj;
                return ScriptBytecodeAdapter.invokeMethodOnCurrentN(class3, (GroovyObject)ScriptBytecodeAdapter.castToType(this, groovy.lang.GroovyObject.class), "println", new Object[] {
                    x.get()
                });
            }
...
            private Reference x;
...
            public _run_closure1(Object _outerInstance, Object _thisObject, Reference x)
            {
                Class class1 = test2$_run_closure1.class;
                Class class2 = groovy.lang.MetaClass.class;
                super(_outerInstance, _thisObject);
                x;
                (Reference)x;
                this;
                JVM INSTR swap ;
                x;
                JVM INSTR pop ;
            }
        }

        ScriptBytecodeAdapter.invokeMethodN(class1, new Integer(10), "times", new Object[] {
            new _run_closure1(this, this, x)
        });
...
        return null;
    }
...
}

てな感じ。
ポイントを3つ挙げるとすれば

  • クロージャ内部で参照してる外側の x への参照を、クロージャを生成する際に渡してる
  • クロージャは内部の private フィールドで x への参照を保持する形になっている
  • クロージャの内部に書かれているコードが変換された doCall メソッドでは、private フィールドに保持している x の参照を使って処理を行っている

ってな感じですね。
クロージャの外に y っての増やしたら、クロージャの private フィールドに y が増えて、コンストラクタで y の Reference も受け取るようになりました。
つまりは、単純な呼び出しは参照を渡してやってるだけっぽいのがわかったんですが、そうなるとさらなる疑問が・・・
今は this.参照 な使い方だったからこれで納得なんですが、 this.メソッド な場合はどーなるんじゃい、と。
というわけで、p.122 の リスト 5.8 クロージャのスコープを調べる を参考に以下の様なスクリプトを作ってみた (`・ω・´)

class Hoge {
  int a = 0
  int b () {
    return 1
  }
  Closure createClosure (int c) {
    int d = 3
    return { caller ->
      [this, this.a, this.b(), c, d, caller, owner]
    }
  }
}

hoge = new Hoge ()
result = hoge.createClosure(2).call(this)

assert result[0] == hoge
assert result[1] == 0
assert result[2] == 1
assert result[3] == 2
assert result[4] == 3
assert result[5] == this
assert result[6] == hoge

assert result[0] == result[6]

コード的には激しく直感的に動いてくれてるんですが、中で何がおこってるのかさっぱり想像がつかん。。。
そんなこんなで jad で逆コンパイルしてやると

import groovy.lang.*;
import java.io.File;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;

public class test2 extends Script
{
...
    public Object run()
    {
        Class class1 = test2.class;
        Class class2 = groovy.lang.MetaClass.class;
        Object obj = ScriptBytecodeAdapter.invokeNew0(class1, Hoge.class);
        Object _tmp = obj;
        ScriptBytecodeAdapter.setGroovyObjectProperty(obj, class1, this, "hoge");
        Object obj1 = ScriptBytecodeAdapter.invokeMethodN(class1, ScriptBytecodeAdapter.invokeMethodN(class1, ScriptBytecodeAdapter.getGroovyObjectProperty(class1, this, "hoge"), "createClosure", new Object[] {
            new Integer(2)
        }), "call", new Object[] {
            this
        });
...
    }
}

という感じのと

import groovy.lang.*;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class Hoge
    implements GroovyObject
{
...
    public int b()
    {
        Class class1 = Hoge.class;
        Class class2 = groovy.lang.MetaClass.class;
        return DefaultTypeTransformation.intUnbox((Integer)ScriptBytecodeAdapter.castToType(new Integer(1), java.lang.Integer.class));
    }

    public Closure createClosure(int i)
    {
        DefaultTypeTransformation.box(i);
        JVM INSTR new #85  <Class Reference>;
        JVM INSTR dup_x1 ;
        JVM INSTR swap ;
        Reference();
        Integer c;
        c;
        Class class1 = Hoge.class;
        Class class2 = groovy.lang.MetaClass.class;
        Integer d = new Reference(new Integer(3));
        class _createClosure_closure1 extends Closure
            implements GeneratedClosure
        {

            public Object doCall(Object obj)
            {
                obj;
                JVM INSTR new #31  <Class Reference>;
                JVM INSTR dup_x1 ;
                JVM INSTR swap ;
                Reference();
                Object caller;
                caller;
                Class class3 = Hoge$_createClosure_closure1.class;
                Class class4 = groovy.lang.MetaClass.class;
                return ScriptBytecodeAdapter.createList(new Object[] {
                    getThisObject(), ScriptBytecodeAdapter.getGroovyObjectProperty(Hoge.class, ((GroovyObject) (getThisObject())), "a"), ScriptBytecodeAdapter.invokeMethodOnCurrent0(Hoge.class, (GroovyObject)ScriptBytecodeAdapter.castToType(getThisObject(), groovy.lang.GroovyObject.class), "b"), c.get(), d.get(), ((Reference) (caller)).get(), ScriptBytecodeAdapter.getGroovyObjectProperty(class3, this, "owner")
                });
            }
...

            private Reference d;
            private Reference c;

            public _createClosure_closure1(Object _outerInstance, Object _thisObject, Reference d, Reference c)
            {
                Class class1 = Hoge$_createClosure_closure1.class;
                Class class2 = groovy.lang.MetaClass.class;
                super(_outerInstance, _thisObject);
                d;
                (Reference)d;
                this;
                JVM INSTR swap ;
                d;
                JVM INSTR pop ;
                c;
                (Reference)c;
                this;
                JVM INSTR swap ;
                c;
                JVM INSTR pop ;
            }
        }
...
    }
...
    private int a;
...
}

という感じのコードが出てきます。
上記のコードをふまえて考えてみると

  • クロージャ内部から、そのクロージャを宣言してる箇所のスコープに存在するインスタンスを利用する場合は、クロージャのコンストラクタに該当インスタンスの参照が渡されるのでそれを利用する
  • クロージャ内部から、そのクロージャを宣言してる箇所より外側のインスタンスやメソッドを利用する場合は、ScriptBytecodeAdapter#getGroovyObjectProperty や ScriptBytecodeAdapter#invokeMethodOnCurrent0 を利用して対象となるインスタンスの Class と、そのインスタンス自身と、対象となるプロパティ名やメソッド名を元にリフレクションパワーでごにょって、結果として帰ってくる Object 型のインスタンスを利用する
  • クロージャ内部で単純に this すると getThisObject() に置き換えられる

てな感じっぽいですね。
リフレクションパワー全開だなぁ (*ノωノ)


ちなみになんですが、クロージャの中で明示的な this 呼び出ししないとどーなるのかも気になって試してみた所、出力されるバイトコードが変わりました。
スクリプトを

class Hoge {
  int a = 0
  int b () {
    return 1
  }
  Closure createClosure (int c) {
    int d = 3
    return { caller ->
      [this, a, b(), c, d, caller, owner]
    }
  }
}

hoge = new Hoge ()
result = hoge.createClosure(2).call(this)

assert result[0] == hoge
assert result[1] == 0
assert result[2] == 1
assert result[3] == 2
assert result[4] == 3
assert result[5] == this
assert result[6] == hoge

assert result[0] == result[6]

という感じにしてみたら(a と b() の呼び出しから this を消した)、jad の出力結果が

import groovy.lang.*;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class Hoge
    implements GroovyObject
{
...
    public Closure createClosure(int i)
    {
...
        class _createClosure_closure1 extends Closure
            implements GeneratedClosure
        {
            public Object doCall(Object obj)
            {
                obj;
                JVM INSTR new #31  <Class Reference>;
                JVM INSTR dup_x1 ;
                JVM INSTR swap ;
                Reference();
                Object caller;
                caller;
                Class class3 = Hoge$_createClosure_closure1.class;
                Class class4 = groovy.lang.MetaClass.class;
                return ScriptBytecodeAdapter.createList(new Object[] {
                    getThisObject(), ScriptBytecodeAdapter.getGroovyObjectProperty(class3, this, "a"), ScriptBytecodeAdapter.invokeMethodOnCurrent0(class3, (GroovyObject)ScriptBytecodeAdapter.castToType(this, groovy.lang.GroovyObject.class), "b"), c.get(), d.get(), ((Reference) (caller)).get(), ScriptBytecodeAdapter.getGroovyObjectProperty(class3, this, "owner")
                });
            }
...
        }
...
    }
}

てな感じに。
ScriptBytecodeAdapter#getGroovyObjectProperty と ScriptBytecodeAdapter#invokeMethodOnCurrent0 に渡されてる Class が変わってますねー。
ここまで追ってみて初めて p.122 の一番下の方に書いてある

クロージャは、すべてのメソッド呼び出しをdelegateオブジェクトと呼ばれるオブジェクトに委譲します。
デフォルトでは宣言者オブジェクト(つまりowner)です。
※) 厳密に言うとすべてのメソッド呼び出しではなく、クロージャ自身で応えられないメソッド。

という記述の意図が分かった (`・ω・´)
this 呼び出しを明示的にした場合、その呼び出し先が owner(ここだと Hoge) である事が自明だから、呼び出し先の Class として Hoge.class を利用していて、明示的に this 呼び出し行わなかった場合は、クロージャ自身が持ってるメソッドか owner が持ってるメソッドかが分からないから、まずはクロージャに委譲して、クロージャが対処出来ないメソッドだった場合はさらにそのクロージャの owner に委譲されるのか!


なるほどねー!
上手く作ってあるなぁ・・・
とりあえずすっきりした (`・ω・´)