メソッド定義にリテラルを含む際の注意


結城浩のSICP日記 - クイズ とそれへの shiro さんのコメントを読んでいて、SqueakVisualWorksSmalltalk でも似たようなことがあるな…と思いました。


後述の VisualWorks の場合とは異なり、SqueakSmalltalk では、リテラルの書き換えも自由に行なえるため、くだんの hyuki さんのエントリーの quiz を施したのと同種の挙動をさせられます。

Object >> foo
   ^ #(0)
Object new foo   " => #(0) "
Object new foo at: 1 put: 1.
Object new foo   " => #(1) "


ところで、LISPScheme と違い、メソッド(つまり、関数)が必ずクラスに属し、そしてクラス以外には属さないことが定められている Smalltalk では、この制約を利用して、メソッド定義時のソースをキャッシュしておき、いつでも参照できるようにする仕組みが設けられています。システムブラウザなどでのソース閲覧も、原則としてこの機構を用いています。

しかし、キャッシュという性格上、前述のような操作の結果はソース閲覧時には反映されません。

(Object >> #foo) getSourceFromFile  " => 'foo ^ #(0)' "

したがって、もし、前述のような操作の結果により問題が生じていた場合でも、その原因究明に思わぬ時間を費やしてしまったりします。


なお、実際を反映した結果を得ることそれ自体は、メソッドの逆コンパイルにより可能です。

(Object >> #foo) decompileString    " => 'foo ^ #(1)' "

システムブラウザで同じことをしたければ、ウインドウ中段のオプションボタン群右端にある、普段は source とラベルされた What to show ポップアップから decompile を選んでおきます(Squeak の場合)。その都度、デコンパイルした結果がコード表示枠に呼び出されます。


もちろん、hyuki さんの出題にもあるように、対象となる配列などをリテラルではなく、動的に生成するようあらかじめ定義しておけば、こうした“問題”の回避は可能です。

Object >> bar
   ^ Array with: 0
Object new bar   " => #(0) "
Object new bar at: 1 put: 1.
Object new bar   " => #(0) "


あえて、リテラル記述の利便性をとるなら、コピーを返すようにすればよいでしょう。

Object >> baz
   ^ #(0) copy
Object new baz   " => #(0) "
Object new baz at: 1 put: 1.
Object new baz   " => #(0) "


なお、ユーザーに多くの自由を許す Squeak と異なり、VisualWorksSmalltalk では、リテラル式で生成された配列や文字列はイミュータブルなので、それへの破壊的操作はエラーになります。

#(0) at: 1 put: 1   " => Unhandled exception: Core.NoModificationError "