Classbox のメソッド探索ルール


Ruby会議で飛び出た Ruby 2.0 の予定。目玉に Smalltalk の特に Squeak で初めて実装が試されたゆかりの機能が2つもあって、Squeakファンとしては実にwktkでした。個人的にも、札幌Ruby会議02での こちら も口から出任せにならずに済みそうでよかったなーとひと安心。w

で、関連してこんなつぶやきを見かけたので、Squeak の Classbox の実装を久しぶりに引っ張り出して調べてみました。

あれ? 本家のclassboxのメソッド探索ルールがいまいちわからない。静的スコープでの解消じゃないんだね。じゃ、動的スコープってこと?

Twitter / yukihiro_matz


論文はいくつかありますが、こちらが比較的くわしいかと。

  • Scoped and Dynamic Aspects with Classboxes [PDF]


Classbox(個人的には、Traits 同様、小文字の単数形はエンティティを表わし、機構としては大文字の複数形で Classboxes を使う方が好みですが―)の動作確認のために Squeak Smalltalk で組まれたコードは、現在でも試作段階のものながら、SqueakMap や SqueakSource などのパッケージシステム経由から入手可能です。ただ、これらのパッケージシステムになじみのない場合は、開発が進められていた 2004年ごろに配布されていた classboz.zip を入手して使用するのがよいでしょう。それでも、ある程度情報を得ようと思ったら、Squeak Smalltalk 環境の使い方をそれなりに知っている必要はありますが…。^^;


classbox.zip を展開し、ホストOS 向け(Win、Mac、Linux)の仮想マシンを別途 ftp.squeak.org より入手、同じディレクトリに入れ、classbox.image をその仮想マシンににドロップインするのが簡単でしょう。配布されている classbox.image は、Squeak3.7 当時のものなので、仮想マシンもそのバージョンに合わせます。


メソッド探索のルールについては、テスト向けに書かれた LocalRebindingTests の #testLocalRebinding2 や #testLocalRebindingDiamond1 あたりが参考になるように思います。

LocalRebindingTests >> testLocalRebinding2
"self run: #testLocalRebinding2"

    | cx1 cx2 |
    self switchToLocalRebinding.
    cx1 := self createClassboxNamed: #CB1.
    cx2 := self createClassboxNamed: #CB2.
    
    cx1 import: #Object from: system.
    cx1 createClassNamed: #A.
    cx1 createClassNamed: #B.
    cx1 createClassNamed: #C.
    cx1 createClassNamed: #D.
    cx1 addMethod: 'foo ^ 10' for: #A.
    cx1 addMethod: 'bar ^ self foo ' for: #A.
    cx1 addMethod: 'foo ^ A new foo' for: #B.
    cx1 addMethod: 'foo ^ B new foo' for: #C.
    cx1 addMethod: 'foo ^ C new foo' for: #D.

    cx2 import: #A from: cx1.
    cx2 import: #D from: cx1.
    cx2 addMethod: 'foo ^ 20' for: #A.
    
    self assert: (cx1 evaluate: 'A new bar') = 10.
    self assert: (cx1 evaluate: 'D new foo') = 10.
    
    self assert: (cx2 evaluate: 'A new bar') = 20.
    self assert: (cx2 evaluate: 'D new foo') = 20.
LocalRebindingTests >> testLocalRebindingDiamond1
    | cx1 cx2 cx3 cx4 |
    cx1 := self createClassboxNamed: #CX1.
    cx2 := self createClassboxNamed: #CX2.
    cx3 := self createClassboxNamed: #CX3.
    cx4 := self createClassboxNamed: #CX4.
    
    cx1 import: #Object from: system.
    cx1 createClassNamed: #A.
    cx1 addMethod: 'foo ^ 1' for: #A.
    cx1 addMethod: 'bar ^ self foo' for: #A.
    
    cx2 import: #A from: cx1.
    cx2 addMethod: 'foo ^2' for: #A.
    cx2 createClassNamed: #B superclass: #A.
    
    cx3 import: #A from: cx1.
    cx3 addMethod: 'foo ^3' for: #A.
    cx3 createClassNamed: #C superclass: #A.
    
    cx4 import: #B from: cx2.
    cx4 import: #C from: cx3.
    self assert: (cx4 evaluate: 'B new bar') = 2.
    self assert: (cx4 evaluate: 'C new bar') = 3.
    
    cx4 import: #A from: cx1.
    self assert: (cx4 evaluate: 'B new bar') = 1.
    self assert: (cx4 evaluate: 'C new bar') = 1.


なにぶん構文が用意されておらず、手続き的で野暮ったいので、やっていることを無理矢理 Ruby っぽく書き下してみたのが以下です。

#testLocalRebinding2

classbox CB1
  import Object

  class A
    def foo; 10 end
    def bar; foo end
  end

  class B
    def foo; A.new.foo end
  end

  class C
    def foo; B.new.foo end
  end

  class D
    def foo; C.new.foo end
  end
end

classbox CB2
  import A, D from CB1

  class A
    def foo; 20 end
  end
end

assert_equal( CB1.classbox_eval{ A.new.bar }, 10 )
assert_equal( CB1.classbox_eval{ D.new.foo }, 10 )

assert_equal( CB2.classbox_eval{ A.new.bar }, 20 )
assert_equal( CB2.classbox_eval{ D.new.foo }, 20 )
#testLocalRebindingDiamond1

classbox CX1
  import Object

  class A
    def foo; 1 end
    def bar; foo end
  end
end

classbox CX2
  import A form CX1

  class A
    def foo; 2 end
  end

  class B < A; end
end

classbox CX3
  import A form CX1

  class A
    def foo; 3 end
  end

  class C < A; end
end

classbox CX4
  import B from CX2
  import C from CX3
end

assert_equal( CX4.classbox_eval{ B.new.bar }, 2 )
assert_equal( CX4.classbox_eval{ C.new.bar }, 3 )

classbox CX4; import A from CX1 end
assert_equal( CX4.classbox_eval{ B.new.bar }, 1 )
assert_equal( CX4.classbox_eval{ C.new.bar }, 1 )


なにかの参考になればさいわいです。


余談として、これを書いているときに気がついたのですが、Squeak 上での実装は、バージョン150 を境に、(個人的には気に入っていた―)動的な性格がごっそりとそぎ落とされてしまったらしいことを見つけました。

たとえば、以前はカレントスレッドがコンテキストとして classbox を持つことができて、それを適宜切り替えることで挙動を変化させることが可能だったはずなのですが、150 より後ではそうした機構は失われていて、もっぱらクラスが持っている「どの classbox からインポートされたか」という情報を元に静的に解決される機構になっています。個人的にはちょっとがっかりですが、何かしら困難があったか、あるいは、他の言語で実装しやすいおとなしい仕様に落ち着いたのかもしれませんね。