SmalltalkとRubyのオブジェクトの動的性比較とそのからくり

  • 15
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

前日と同じく RubyKaigi で話題に上った「Ruby は Smalltalk ほど動的ではない」に絡めて少し。

すべての基本であるオブジェクトが何でできているか、から考えると言語の動的性の程度を比較しやすいと思います。

Ruby のオブジェクトはハッシュと呼ばれる連想配列(Smalltalk や Python 風に言うと辞書)のような振る舞いをするので、インスタンス変数は必要になったときに追加されます。すごく動的ですね。

>> class Foo; attr_accessor :bar, :baz end
=> nil
>> foo = Foo.new
=> #<Foo:0x00000600443df0>
>> p foo.instance_variables
[]
=> []
>> foo.bar = 1
=> 1
>> p foo.instance_variables
[:@bar]
=> [:@bar]
>> foo.baz = 2
=> 2
>> p foo.instance_variables
[:@bar, :@baz]
=> [:@bar, :@baz]

Ruby のオブジェクトにとって、インスタンス変数というのはクラスで定められる属性ではなく、必要なときにどんどん追加されるもののようです。

>> foo.instance_variable_set(:@qux, 3)
=> 3
>> foo.instance_variables
=> [:@bar, :@baz, :@qux]

これは、SELF やその影響下にある JavaScript などのプロトタイプベースOOPL におけるオブジェクトの振る舞いとよく似ています。

function Foo() {};

var foo = new Foo();

print( "[" + Object.getOwnPropertyNames(foo) + "]" );
//=> []

foo.bar = 1;
foo.baz = 2;
foo.qux = 3;

print( "[" + Object.getOwnPropertyNames(foo) + "]" );
//=> [bar,baz,qux]

Ruby のオブジェクトのこのような振る舞いに対して、Smalltalk のオブジェクトは固定長の配列のような振る舞いをします。そう、Smalltalk のオブジェクトにとって、インスタンス変数はクラスにより定められるものであり、生まれたときに決められていて原則として死ぬまで増やせない構造になっているのです。

動的性の観点からは、どう見ても Ruby の圧勝です。本当にありがとうございました。


実際に確かめてみましょう。以下は Squeak Smalltalk での話ですが、他の Smalltalk でもそんなに変わらないと思います。

Object subclass: #Foo
    instanceVariableNames: 'bar baz'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'UserObjects'
Foo compile: 'bar: val bar := val'.
Foo compile: 'bar ^foo'.
Foo compile: 'baz: val baz := val'.
Foo compile: 'baz ^baz'.
| foo |
Object new instanceVariableValues asArray.  "=> #() "
foo := Foo new.
foo instanceVariableValues asArray.  "=> #(nil nil) "
foo bar: 1; baz: 2.
foo instanceVariableValues asArray.  "=> #(1 2) "
foo instVarAt: Foo instSize.  "=> 2 "
[foo instVarAt: Foo instSize + 1].  "value => Error: subscript is out of bounds: 1 "
foo instVarNamed: #bar.  "=> 1 "
[foo instVarNamed: #qux].  "value => Error: no such inst var "
foo inspect.

dynamicLateBindingsOfSt01.png


でも、安心してください。ここで簡単に諦めないのが Smalltalk 。通常の言語では考えられないような  汚い  奥の手を使って、ちゃんと“動的”してます。

最初の Foo の定義の式を次のように変更(インスタンス変数 qux を追加)して評価(do it)してみましょう。

Object subclass: #Foo
    instanceVariableNames: 'bar baz qux'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'UserObjects'

直前のスクリプト、最後の式で開いた Foo のインスタンスのインスペクタの画面をよく見ていてください。

dynamicLateBindingsOfSt02.png

なんか事も無げに増えましたね。


同名クラスを新たに定義することで、扱えるインスタンス変数を増やすことができるのはわかりやすいですが、それ以降に生成されるインスタンスはともかく、既存のインスタンスにその影響が及ぶというのは、ちょっとオカルトじみていますね。

もちろんクラスへ直接メッセージを送ることでインスタンス変数を増やすことも可能です。

Foo addInstanceName: 'quux'

dynamicLateBindingsOfSt03.png


いよいよどんな仕組みでこれをやっているのか、わからなくなります。

ちょっと調べてみましょう。

Foo addInstVarName: 'quuux'

といったような式を入力して、右クリック → debug it を選びます。

dynamicLateBindingsOfSt04.png

デバッガーが起動するので、Into ボタン(キーになる処理を行なっていると思われる式のとき)や Throught ボタン(そうでないとき)をクリックして処理を進めます。

dynamicLateBindingsOfSt05.png

dynamicLateBindingsOfSt06.png

うまく処理を追っかけられると、新しいクラスが古いクラスとどのように置き換えられ、それに伴ってインスタンス群も刷新される様子が手に取るようにわかります。Foo のインスタンスをインスペクトするウインドウを同時に観察できるように配置しておけば、インスタンス変数が追加される瞬間(実際は、新しいクラスのインスタンスに置き換えられる瞬間)が見て取れます。

これには #become: という特殊な機能が使われています。

| obj1 obj2 |
obj1 := 'string'.
obj2 := 1.234.
obj1 become: obj2.
{obj1. obj2}  "=> #(1.234 'string') "

(あいにく、手元の Windows 版 Squeak 5.0 では #become: が正常に動作しないので、上は 4.3J での結果です。)

端的には、古いクラスのインスタンスを新しいクラスのインスタンスとして複製し、それを古いクラスのインスタンスと #become: で入れ替えているのですね。新しいクラスそれ自体も古いクラスに #become: されて、すべてが我々ユーザーが知らぬ間に置き換えられます。おそろしい…。

ともあれ、かくもトリッキーな方法ではありますが、Smalltalk は本来は固定長の配列のような振る舞いをするオブジェクトを使いながらも、クラスの定義を変更したり、その変更に既存のインスタンスが追従して、インスタンス変数を追加したりできるようになっているわけです。


無茶しやがって…

この投稿は Smalltalk Advent Calendar 201515日目の記事です。