Squeak Smalltalk における文字列リテラルとその同一性

次は真だろうか。

"abc" == "abc"

勿論、真である。RubyではString#==はオーバーライドされていて、長さと内容が等しい文字列同士を比較すると真になる。

さて、次は真だろうか。

"abc".equal? "abc"

これはたぶん偽だろう。それは何故か。Object#equal?は引数のオブジェクトがレシーバ自身である、まったく同一のオブジェクトを指すときのみ、真となる。

ratio - rational - irrational : シンボルとは何か その1(前編) - 文字列の同一性と同値性


Squeak Smalltalk では Java などと同様に、Ruby での結果と異なり、両方とも真になります(Smalltalk では同値性チェックには #= を、同一性では #== をコールします。念のため)。

{'abc' = 'abc'.  'abc' == 'abc'}   "=> #(true true) "


けっして同値性と同一性が混同されているわけではないことは、一方をコピーしてみれば分かります。

{'abc' = 'abc' copy.  'abc' == 'abc' copy}   "=> #(true false) "


どうしてこうなるのでしょう。コードのコンパイル結果を見ると原因らしきものを見て取ることができます。

['abc' == 'abc'] method symbolic
=> '...
   30 <22> pushConstant: ''abc''
   31 <22> pushConstant: ''abc''
   32 <C6> send: ==
   ...' "


30、31 バイト目で文字列 「abc」をスタックに push していますが、このとき同じバイトコード <22> が二度使われています。念のため同値でない場合には、

['abc' == 'abcd'] method symbolic
=> '...
    34 <22> pushConstant: ''abc''
    35 <23> pushConstant: ''abcd''
    36 <C6> send: ==
    ...'

のように別のバイトコードが使われます。


つまり、'abc' == 'abc' の最初(レシーバ)の 'abc' と、二番目(引数)の 'abc' はバイトコードの時点ですでに区別されていないことになります。したがって、結果が真になるのは必然です。


ではどの時点で両者は区別されなくなるのか、コンパイルバイトコード生成)の一つ手前、パース時の動きを追ってみます。

Parser new halt parse: 'DoIt ''abc'' == ''abc''' class: UndefinedObject


halt の送信により呼び出されたプレデバッグ・ウインドウ内の「UndefinedObject>>DoIt」をクリックしてデバッガを起動し、あとは適当なタイミング(下図の最上段枠内を参照)で Through(逐次実行)と Into(ダイブ)を使い分けながら動きを追ってゆくと、スキャナ(字句解析器)がリテラルより生成した文字列オブジェクト自身を「キー」に、それを指すリーフノードを「値」にして登録した辞書を参照していることが分かります。

http://squab.no-ip.com/collab/uploads/61/encoder.png


なるほど、これなら二回目には初出の同値オブジェクトが再利用されるコードになりそうです。試しにこのリーフノードのキャッシュに使っているリテラル辞書のスーパークラスを、キーとなるオブジェクトの同値性で区別する Dictionary から、同一性で区別する IdentityDictionary に差し替え、さらに同値性を確認するためのメソッド #literalEquality:and: を書き換えて、同値ではなく同一性でキーを判別するようにしてみましょう。

IdentityDictionary subclass: #LiteralDictionary
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Compiler-Support'
LiteralDictionary >> literalEquality: x and: y
    ^ (x class = Array and: [y class = Array])
        ifTrue: [self arrayEquality: x and: y]
        ifFalse: [(x class == y class) and: [x == y]]


こうすると、冒頭の式の結果は Ruby のそれと同じになります。

{'abc' = 'abc'.  'abc' == 'abc'}   "=> #(true false) "


もちろん、バイトコードも上の仕様変更を反映したものになっています。

['abc' == 'abc'] method symbolic
=> '...
    34 <22> pushConstant: ''abc''
    35 <23> pushConstant: ''abc''
    36  send: ==
    ...'


しくみがわかると楽しいですね。