Smalltalk の nil とその振る舞い
Ruby の開発者向けのメーリングリストや、2ちゃんねるの関連スレで、nil の文字列化(あるいは文字列型への変換)に関する仕様変更についての話題で、いつになく活発に議論が展開されているのに触発されたので、Smalltalk の nil やその振る舞いについて、Ruby や LISP との比較がてら、特徴を整理してみました。
nil とは何か?
Ruby では nil は、クラス「NilClass」のインスタンスです。気持ちは分かるのですが、これだと、じゃあ nil って何よ…的な感じがしていけません。Smalltalk では「UndefinedObject」のインスタンスとすることで、その役割が比較的、明確(?)にクラス名に込められています。と、言っても、UndefinedObject として定義されているのに未定義とはこれいかに…という意味での“未定義”ではなく、変数や配列の要素が未定義(もうちょっと踏み込むと、未初期化)時に使用されるオブジェクト…という意味でしょうね(ぜんぜん明確じゃないじゃん…w)。
ともあれ、Smalltalk において、宣言だけして初期化(最初の代入)がまだ済んでいない変数には、かならずこの nil が関連づけられて(代入されて)います。これは Ruby と同じです。
"Smalltalk" | foo | " テンポラリ変数の宣言 " foo " => nil "
#Ruby def getFoo; @foo end #インスタンス変数へのアクセッサの定義(わかりやすいよう get を付けました) getFoo #=> nil
ただし、Smalltalk のグローバル変数やプール変数(複数の、しかし、決められたクラス間でのみ通用するグローバル変数)、Ruby のテンポラリ変数については、値との関連づけ(代入)が宣言を兼ねるので、例外です。
変数だけでなく、配列の要素についても同様のことが言えます。
"Smalltalk" Array new: 3 " => #(nil nil nil) "
#Ruby Array.new(3) #=> [nil, nil, nil]
こうした背景もあって、とくに Smalltalk の実行時のエラーによる中断のほとんどが、変数の初期化忘れなどの結果、この nil へメッセージが送信された際に nil が発する「そんなメッセージ、あたしゃ知らねーョ!」的エラーにより引き起こされます。
| foo | foo + 4 " => Error -- MessageNotUnderstood: UndefinedObject >> + "
nil の特別扱い
よく知られているように、Smalltalk には Ruby を含む通常の言語には普通に用意されている制御構造構文のようなものはなく、すべてをメッセージ式、たとえば条件分岐なら真偽値へのメッセージングのかたちで表現します(あくまで“表現”だけで内部、つまり、バイトコードレベルでは違いますが…)。
3 < 4 ifTrue: [5] ifFalse: [6] " => 5 "
という式ならば、3 < 4 の結果である true に対して、「ifTrue: [5] ifFalse: [6]」というメッセージが送られて、結果の 5 が返る…というふうに(あくまで字面的には…くどいっ!)解釈します。
これは true か false かという局面ですが、nil かそうではないか、つまり、未定義か否かという局面のための専用の条件分岐式も Smalltalk では用意されています。nil は特別扱いされているわけです。
nil ifNil: [#undefined] ifNotNil: [#defined] " => #undefined "
たとえば、変数(主にインスタンス変数)が初期化済みならそれを、まだならしかるべきオブジェクトと関連づけてからそれを返す…という、よくある遅延初期化のイディオムは、この nil 専用の制御構造を活用したもののひとつです。(わかりやすいように get を付けていますが、こうした命名は推奨されません。)
Foo >> getInstVar ^ instVar ifNil: [instVar := #something]
Foo new getInstVar " => #something "
Ruby では、nil に対して false と同じ振る舞いをする性質が与えられているせいもあってか、このような専用の制御構造構文をわざわざ用意するようなことはしていません。もっとも、Ruby の nil には、Smalltalk の nil よりも、LISP の NIL や他の言語の同種オブジェクトに近い性格が与えられていると考える方が無難かも。
; LISP (if nil 'defed 'undefed) ;=> UNDEFED
" Smalltalk " nil ifTrue: [#defed] ifFalse: [#undefed] " => Error "
# Ruby if nil then :defed else :undefed end #=> :undefed nil ? :defed : :undefed #=> :undefed
ちなみに LISP で NIL は、この false の他に、空リストの意味も持ちます。
(eq nil '()) ;=> T (念のため、T は LISP で“真”)
これと関係あるよでないよな話ですが、Smalltalk でも、空コレクション(空配列や空文字列)か nil ならば…という判断を要する局面は意外と多いため、冗長さがウリ(節操のない…とも言うw)の Squeak では #isEmptyOrNil というヒューメインwなメソッドが用意されています。
nil isEmptyOrNil " => true " #() isEmptyOrNil " => true " '' isEmptyOrNil " => true " 123 isEmptyOrNil " => Error -- コレクションか nil にしか使えない " $a isEmptyOrNil " => Error " 'a' isEmptyOrNil " => false "
前後しますが、現行の Ruby では、to_s のレシーバが nil でも "" でも、同じ "" を返すことを利用して、同様の局面でも、empty? ひとつで済ませることができます。nil.to_s が "nil" を返すようになると、このイディオムが使えなくなる…というのが今回の仕様変更に対する反対派の理由のひとつとしてあるようです。
nil の文字列化は?
現在の Ruby では、nil の文字列表現は "nil"で、String 型への変換は空文字列 "" です。これを後者においても、"nil" を返すようにするというのが、今回の話題の中心となっているようです。
nil.inspect #=> "nil" nil.to_s #=> ""
Smalltalk では、表現、型変換ともに、Ruby の仕様変更後案と同じく、文字列 'nil' に変換されます。(ただし、Squeak の場合。VisualWorks や ANSI 準拠のお手製 Smalltalk では、Object >> #asString が定義されていないため、後者ではエラーになります。)
nil printString " => 'nil' " nil asString " => 'nil' "
printString はオブジェクトを文字列で表現する際に送るメッセージです(実質的な仕事は #printOn: が担当)。Ruby では inspect にあたりますが、Smalltalk で inspect と言えばインスペクタ(オブジェクトの属性ブラウザ)の起動なので、なんかややこしいですね(^_^;)。一方の asString は“String 型への変換”を行ないます。もっとも、#asString はデフォルト(Object >> #asString)では、たんに #printString のエイリアスとして定義されているだけなので、多くのレシーバはいずれのメッセージに対しても同じ結果を返します。このため、両者はおうおうにして混同されがちです。(追記:[ruby-dev:29448] によれば、もとをただすと、to_s は Smalltalk の #printString を、inspect はそのまま #inspect を意識して作られたものだったようです。いずれにせよ、設計にかかわった人たちの間ですら、この件、ちょっと解釈が分かれ気味のようですね。)
余談ですが、#asString が #printString とは違う結果を返すオブジェクトが属するクラスを探すのは簡単です。Squeak なら、適当な場所で「asString」とタイプして入力し(必要なら選択してから)、implementors of it (alt/cmd + m) します。表示される一覧の Object >> #asString 以外がそうです。たとえば、リストのトップにくる a ByteArray ならこんな感じ。
#(97 98 99) asByteArray printString " => 'a ByteArray(97 98 99)' " #(97 98 99) asByteArray asString " => 'abc' "
Ruby と Smalltalk で共通のオブジェクトだと、シンボル(a Symbol)の例なんかいかがでしょう。
:sym.inspect #=> ":sym" :sym.to_s #=> "sym"
#symbol printString " => '#symbol' " #symbol asString " => 'symbol' "
これら asString、printString とは別に、Smalltalk には、オブジェクトを Smalltalk コードで表現したいときに送る storeString なんていう、なんちゃってシリアライザみたいなのもあります(なんちゃって…なので、レシーバによっては必ずしも上手く動作しません(^_^;))。
#(97 98 99) asByteArray storeString
=> '((ByteArray new: 3) at: 1 put: 97; at: 2 put: 98; at: 3 put: 99; yourself)'
閑話休題。Smalltalk の asHoge は Ruby の to_x に相当し、指定した Hoge 型に変換したオブジェクトを返してきます。to_s は asString、to_i は asInteger、to_a は asArray といったところでしょうか。
ところで、この種の asHoge メッセージにより起動されるメソッド #asHoge は、ケント・ベックのパターンにおいて「Converter Method」に分類されるもので、便利で簡潔な記述を可能とする反面、プロトコルの爆発を引き起こすため、むやみやたらな定義は慎むべきとされています。なお、ベックは、このパターンの代替えとして「Converter Constructor Method」の使用を推奨しています。
で、その「Converter Constructor Method」に相当するものは、Ruby では String()、Smalltalk では String with: ... や String withAll: ... にあたるわけですが、Ruby と違い Smalltalk では、ここで nil や nil を含む配列を指定することはできません。もっとも、nil だけが駄目…ということではなく、文字オブジェクト以外のものはいずれもNGです。これは、Smalltalk の文字列が「文字のみを要素とする特殊な配列」として実装されていることも無関係ではないでしょうが、それよりも、両言語のポリシーの違いによるところが大きいような気がします。
# Ruby String("a") #=> "a" String(["a","b","c"]) #=> "abc" String(nil) #=> "" String(["a","b",nil]) #=> "ab" String(1) #=> "1" String([1,2,3]) #=> "123"
" Smalltalk " String with: $a " => 'a' " String withAll: #($a $b $c) " => 'abc' " String with: nil " => Error " String withAll: #($a $b nil) " => Error " String with: 1 " => Error " String withAll: #(1 2 3) " => Error "
ちなみに LISP では、文字列表現では "NIL" を、型変換時には "" を返すようです。LISP で NIL は空リストと等価であることを差し引いても、個人的にはこれが自然なように思えます(つまり、Smalltalk のは表現と型変換を混同してしまったダメな例で、Ruby は、別に今のまま変えなくてもいいんじゃないかな…と)。
(write-to-string nil) ;=> "NIL"
(coerce nil 'string) ;=> ""
(string nil) ;=> ""(string nil) ;=> "NIL" でした。ご指摘、多謝。>shiro さん
VisualWorks や ANSI 準拠の Smalltalk の場合
Object >> #toString は実装されていないため、限られたオブジェクトだけが toString メッセージによる String 型への変換が可能になっています。当然、それ以外ではエラーになります。nil もしかりです。