「オブジェクト指向プログラミングとは結局なんなのか」の Smalltalk 関連の用語の使い方についてちょっとだけコメントさせていただいたところで、実はもうひとつ些細な間違いを見つけてしまったのですが、ただそれを直すとなると文章の流れを一部変えないといけないので申し訳ないですし、嘘も方便、わかりやすければ多少の実情との不一致もいいのかな、とあえてスルーした Smalltalk の裏事情をこっそり()こちらに書きたいと思います。
その気になった記述は >重要なのは、booleanに限らずともifTrue:ifFalse:セレクタによるメッセージに返答できれば分岐処理が行えるというところで、理想としてはそうなのですが実際の処理系ではそのようにはなっていません。たとえば直後の
数値型に0か非0かで分岐するメソッドを生やせば数値に対してifTrue:ifFalse:セレクタによるメッセージを送れます
というのをSqueak Smalltalkでやってみると
というふうに、NonBooleanReceiver: proceed for truth. としかられてしまいます。
なぜこういうことが起こるかというと、0 ifTrue: [#nonZero] ifFalse: [#zero] がどのようにコンパイルされているかを見ればわかります。[0 ifTrue: [#nonZero] ifFalse: [#zero]] method symbolic
このように書き加えて alt/cmd + p などで print it してみてください。コンパイル後のバイトコード列とそのメッセージ風ニーモニック表現が表示されます。
33 <8F 00 00 06> closureNumCopied: 0 numArgs: 0 bytes 37 to 42
37 <75> pushConstant: 0
38 <99> jumpFalse: 41
39 <23> pushConstant: #nonZero
40 <90> jumpTo: 42
41 <22> pushConstant: #zero
42 <7D> blockReturn
43 <D1> send: method
44 <D0> send: symbolic
45 <7C> returnTop
37バイト目から41バイト目が試したコードのコンパイル結果ですね。何か気がつきましたか? そう。新たに定義された Integer>>#ifTrue:ifFalse: なんてメソッドはここからはぜんぜんコールなんてされていないのです。
参考まで 1 hoge: 2 fuga: 3 というメッセージ式なら、次のようにコンパイルされます。
[1 hoge: 2 fuga: 3] method symbolic
29 <8F 00 00 05> closureNumCopied: 0 numArgs: 0 bytes 33 to 37
33 <76> pushConstant: 1
34 <77> pushConstant: 2
35 <23> pushConstant: 3
36 <F2> send: hoge:fuga:
37 <7D> blockReturn
38 <D1> send: method
39 <D0> send: symbolic
40 <7C> returnTop
Integer >> ifTrue: nonZeroBlock ifFalse: zeroBlock例によって NonBooleanReceiver: proceed for truth. のノーティファイアは出るものの、デバッガが立ち上がる様子はありません。つまり、せっかく定義した Integer>>#ifTrue:ifFalse: ですが、やはりコールされていないのですね。 勘の働く方ならもうおわかりでしょうが、実は Boolean のサブクラスである True や False に定義されている True>>#ifTrue:ifFalse: や False>>#ifTrue:ifFalse: も、実は今回定義した Integer>>#ifTrue:ifFalse: 同様、コールはされていなかったのです。ΩΩΩ<ナ、ナンダッテーッ! ためしに、True>>#ifTrue:ifFalse: に Integer>>#ifTrue:ifFalse: のときと同様に self halt. を挿入してみましょう。
self halt.
^self = 0 ifTrue: [zeroBlock value] ifFalse: [nonZeroBlock value]
True >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
self halt.
^trueAlternativeBlock value
もし、こいつが本当にコールされていたら、この時点でアウトなはずですが何も起こりませんね。念のため、明示的に True>>#ifTrue:ifFalse: をコールするコードを評価してみます。
true ifTrue: [#true] ifFalse: [#false] "=> #true "
ちなみに今回定義した Integer>>#ifTrue:ifFalse: も、True や False の #ifTrue:ifFalse: もメソッド自体は存在しているので、動的、あるいは静的にコールすることで実行は可能です。
0 perform: #ifTrue:ifFalse: with: [#nonZero] with: [#zero] "動的コール"
(Integer >> #ifTrue:ifFalse:) valueWithReceiver: 0 arguments: {[#nonZero]. [#zero]} "静的コール"
MethodNode >> initialize以上、Smalltalk を例にメッセージングを解説するときの落とし穴でした。 あと、バイトコードを扱ったので、念のためにもうひとつ。
"MessageNode initialize"
MacroSelectors :=
#( ifTrue: ifFalse: ifTrue:ifFalse: ifFalse:ifTrue:
and: or:
whileFalse: whileTrue: whileFalse whileTrue
to todo:
caseOf: caseOf:otherwise:
ifNil: ifNotNil: ifNil:ifNotNil: ifNotNil:ifNil:
repeat ).
MacroTransformers :=
#( transformIfTrue: transformIfFalse: transformIfTrueIfFalse: transformIfFalseIfTrue:
transformAnd: transformOr:
transformWhile: transformWhile: transformWhile: transformWhile:
transformToDo: transformToDo:
transformCase: transformCase:
transformIfNil: transformIfNil: transformIfNilIfNotNil: transformIfNotNilIfNil:
transformRepeat: ).
MacroEmitters :=
#( emitCodeForIf:encoder:value: emitCodeForIf:encoder:value:
emitCodeForIf:encoder:value: emitCodeForIf:encoder:value:
emitCodeForIf:encoder:value: emitCodeForIf:encoder:value:
emitCodeForWhile:encoder:value: emitCodeForWhile:encoder:value:
emitCodeForWhile:encoder:value: emitCodeForWhile:encoder:value:
emitCodeForToDo:encoder:value: emitCodeForToDo:encoder:value:
emitCodeForCase:encoder:value: emitCodeForCase:encoder:value:
emitCodeForIfNil:encoder:value: emitCodeForIfNil:encoder:value:
emitCodeForIf:encoder:value: emitCodeForIf:encoder:value:
emitCodeForRepeat:encoder:value:).
MacroSizers :=
#( sizeCodeForIf:value: sizeCodeForIf:value: sizeCodeForIf:value: sizeCodeForIf:value:
sizeCodeForIf:value: sizeCodeForIf:value:
sizeCodeForWhile:value: sizeCodeForWhile:value: sizeCodeForWhile:value: sizeCodeForWhile:value:
sizeCodeForToDo:value: sizeCodeForToDo:value:
sizeCodeForCase:value: sizeCodeForCase:value:
sizeCodeForIfNil:value: sizeCodeForIfNil:value: sizeCodeForIf:value: sizeCodeForIf:value:
sizeCodeForRepeat:value:).
MacroPrinters :=
#( printIfOn:indent: printIfOn:indent: printIfOn:indent: printIfOn:indent:
printIfOn:indent: printIfOn:indent:
printWhileOn:indent: printWhileOn:indent: printWhileOn:indent: printWhileOn:indent:
printToDoOn:indent: printToDoOn:indent:
printCaseOn:indent: printCaseOn:indent:
printIfNil:indent: printIfNil:indent: printIfNilNotNil:indent: printIfNilNotNil:indent:
printRepeatOn:indent:)
くだんのコメントで指摘した「2というオブジェクトに対して+というメッセージを3を引数に送信」という表現の誤りについてですが(Objective-C 界隈にかたくなにそう信じる宗派が存在するのはさておき)Smalltalk においてバイトコードレベルに限れば、これはあながち間違いでもなさそうです。
[2+3] method symbolic
25 <8F 00 00 04> closureNumCopied: 0 numArgs: 0 bytes 29 to 32
29 <77> pushConstant: 2
30 <22> pushConstant: 3
31 <B0> send: +
32 <7D> blockReturn
33 <D1> send: method
34 <D0> send: symbolic
35 <7C> returnTop
ちゃんと「+」というメッセージを送っているように見えますよね。;p
もちろんこれはあくまでバイトコードレベルに限った話です。オーソドックスな条件分岐があることなどから明らかなように、Smalltalk のバイトコードは、Smalltalk とは別の言語と考えるべきなので、やはり 2 + 3 をメッセージ式と解釈する場合は「 2 への + 3 というメッセージの送信」とするのが正解ですのであしからず。
追記: どうやら #ifTrue:ifFalse: の引数(のうちどちらか)をブロックリテラルにしなければインライン展開はされずに、#ifTrue:ifFalse: をコールする普通のバイトコード列にコンパイルしてくれるようです。[0 ifTrue: [#nonZero] ifFalse: [#zero] yourself] method symbolic
37 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 41 to 56
41 <75> pushConstant: 0
42 <8F 00 00 02> closureNumCopied: 0 numArgs: 0 bytes 46 to 47
46 <23> pushConstant: #nonZero
47 <7D> blockReturn
48 <8F 00 00 02> closureNumCopied: 0 numArgs: 0 bytes 52 to 53
52 <25> pushConstant: #zero
53 <7D> blockReturn
54 <D4> send: yourself
55 <F2> send: ifTrue:ifFalse:
56 <7D> blockReturn
57 <D1> send: method
58 <D0> send: symbolic
59 <7C> returnTop
MessageNode >> transformIfTrueIfFalse: encoder
^(self checkBlock: (arguments at: 1) as: 'True arg' from: encoder maxArgs: 0)
and: [(self checkBlock: (arguments at: 2) as: 'False arg' from: encoder maxArgs: 0)
and: [arguments do: [:arg| arg noteOptimizedIn: self].
true]]