Smalltalk におけるシンボルの振る舞い


すべての話題をSmalltalkに結びつける名人 の称号(違う)をいただいたからには、nil.to_s と同じくらい Ruby の開発者向けメーリングリスト盛り上がっている、シンボルの仕様変更についても言及せねばなりますまい。…と思ったのですが、これについては以前、すでに似たようなテーマで書いてしまったものがあるので、それへのポインタだけ。w


…で終わらるのもなんですので、シンボルと文字列の区別に大きな影響を受けるハッシュテーブルの振る舞いについて、SqueakSmalltalk のそれを、RubyLISP と比較して整理したものも書いてみました。

Smalltalk の辞書について

Smalltalk ではハッシュテーブルを「辞書」と呼んでいます。クラス Dictionary のインスタンス(a Dictionary)で、関連付けオブジェクト(an Association)のみを要素に持つ特殊なセット(a Set)として実装されています。

Dictionary superclass   " => Set "


念のため、関連づけオブジェクトというのは、「キー」と「値」の組のことで、単独では、キーとなるオブジェクトに「-> #値」というメッセージを送信することで生成できます。

#key -> #value                        " => #key -> #value "
Association key: #key value: #value   " => #key -> #value "

また、セットというのは、要素の順番を考慮せず、重複を許さないコレクションのことです。

Set withAll: #(1 1 1 2 2 3)   " => a Set(1 2 3)"
#(1 2 3) first          " => 1 "
#(1 2 3) asSet first    " => Error: "

キーに対応する値の追加(と、アクセス)

SmalltalkRuby では、配列への要素追加(アクセスも…)と、インデックスにキーを使う以外は、同じ要領で行なえます。

#Ruby
h = Hash.new
h[:key1] = :value1
h[:key2] = :value2
p h
=> {:key2=>:value2, :key1=>:value1}
"Smalltalk"
| dict |
dict := Dictionary new.
dict at: #key1 put: #value1.
dict at: #key2 put: #value2.
^ dict
=> a Dictionary(#key1->#value1 #key2->#value2)


LISP ではハッシュテーブルの生成は MAKE-HASH-TABLE で、キーに対応する値の参照には GETHASH、追加には、SETF を使います。

;;Common Lisp
(setq h (make-hash-table))
(setf (gethash 'key1 h) 'value1)
(setf (gethash 'key2 h) 'value2)
(write h)
=> #S(HASH-TABLE :TEST FASTHASH-EQL (KEY2 . VALUE2) (KEY1 . VALUE1))


余談ですが、この SETF は、ちょっと面白い 関数 マクロ で、第二パラメータの式で参照できる要素を置き換える機能を提供します。意外なことに、アラン・ケイ発のアイデアなのだそうで(参考:The Evolution of Lisp (PDF) )。


同内容のシンボルと文字列、それぞれをキーとしたときの振る舞い

h = Hash.new
h[:key] = :value1
h["key"] = :value2
p h
#Ruby 1.8.5
=> {:key=>:value1, "key"=>:value2}
#Ruby 1.9.0
=> {:key=>:value2}
"Smalltalk"
| dict |
dict := Dictionary new.
dict at: #key put: #value1.
dict at: 'key' put: #value2.
^ dict
=> a Dictionary(#key->#value2)

注意:Smalltalk でも処理系によって振る舞いが違います。この例は、SqueakSmalltalk の場合です。

;;Common Lisp
(setq h (make-hash-table :test #'equal))
(setf (gethash 'key h) 'value1)
(setf (gethash "KEY" h) 'value2)
(write h)
=> #S(HASH-TABLE :TEST FASTHASH-EQUAL ("KEY" . VALUE2) (KEY . VALUE1))

コメントでの NANRI さんのご指摘にもあるように、Common Lisp のハッシュテーブルは、デフォルトでは同一かを見ている(EQL)ので、同一性が保証されない文字列をキーに使うことはできません。:test オプション(キーワード引数)で、等価かを見る関数(EQUAL)などに明示的に指定し直す必要があります。


このとき指定できる関数と、それぞれの比較関数の振る舞いは次の通り。

(setq s "hoge")
(eq s s)                ;;=> T
(eq "hoge" "hoge")      ;;=> NIL
(eq 1.2 1.2)            ;;=> NIL
(eql "hoge" "hoge")     ;;=> NIL
(eql 1.2 1.2)           ;;=> T
(equal "hoge" "hoge")   ;;=> T
(equal "Hoge" "hoge")   ;;=> NIL
(equalp "Hoge" "hoge")   ;;=> T

しかし、いずれにしても、同内容であっても型の異なるシンボルと文字列が同一視されることはないので、両者は区別されます。


なお念のため、LISP では、シンボルの記述の解釈、および、シンボルと文字列の比較は次のようになっています。

'key                   ;;=> KEY
'Key                   ;;=> KEY
'KEY                   ;;=> KEY
'|key|                 ;;=> key
'|Key|                 ;;=> Key
(equal "KEY" 'key)     ;;=> NIL
(string= "KEY" 'key)   ;;=> T


STRING= をハッシュテーブルの比較関数として指定できれば、SqueakSmalltalk のような振る舞いをさせることはできそうですが、こうした指定をすることは許されないようです。

(make-hash-table :test #'string=)   ;;=> Error


これとも関連しますが、[ruby-dev:29553] の最後で指摘されているようなことにからめては、Smalltalk には a Dictionary とは別に、与えられたキーを等価かではなく同一かで判断する an IdentityDictionary というオブジェクトがあります。こちらでは LISP(そして、これまでの Ruby)同様、シンボルと文字列とでは同内容のキーでも区別されます。

| idict |
idict := IdentityDictionary new.
idict at: #key put: #value1.
idict at: 'key' put: #value2.
^ idict
=> an IdentityDictionary(#key->#value1 'key'->#value2)

さらに、比較の判断をユーザーが自由に定義できる PluggableDictionary というクラスも用意されています。

Ruby のシンボルの振る舞い

今回の仕様変更で Ruby のシンボルは、これまでの C の enum 的なものから Smalltalk のシンボルっぽいものへの変化してゆくようにも見えます。しかし一方で、次のような挙動が意識してのものならば(少なくとも Squeak とは異なる)独自のポリシーで動いていく可能性が高そうです。

#ruby 1.9.0 (2006-09-08) [i386-cygwin]
"hoge" == :hoge   #=> true
:hoge == "hoge"   #=> false