Smalltalkにおける「すべてがメッセージ」の裏側と例外

  • 3
    Like
  • 2
    Comment
More than 1 year has passed since last update.

オブジェクト指向プログラミングとは結局なんなのか」の Smalltalk 関連の用語の使い方についてちょっとだけコメントさせていただいたところで、実はもうひとつ些細な間違いを見つけてしまったのですが、ただそれを直すとなると文章の流れを一部変えないといけないので申し訳ないですし、嘘も方便、わかりやすければ多少の実情との不一致もいいのかな、とあえてスルーした Smalltalk の裏事情をこっそり()こちらに書きたいと思います。


その気になった記述は

重要なのは、booleanに限らずともifTrue:ifFalse:セレクタによるメッセージに返答できれば分岐処理が行える

というところで、理想としてはそうなのですが実際の処理系ではそのようにはなっていません。たとえば直後の

数値型に0か非0かで分岐するメソッドを生やせば数値に対してifTrue:ifFalse:セレクタによるメッセージを送れます

というのをSqueak Smalltalkでやってみると

iftrueiffalsetruth01.png

というふうに、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:ifFalse: に、self halt. を挿入して accept (alt/cmd + s) し、コールされたらデバッガが立ち上がるようにしておいて、0 ifTrue: [#nonZero] ifFalse: [#zero] を改めて do it (alt/cmd + d) してみます。

Integer >> ifTrue: nonZeroBlock ifFalse: zeroBlock
    
self halt.
    
^self = 0 ifTrue: [zeroBlock value] ifFalse: [nonZeroBlock value]


例によって 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. を挿入してみましょう。

True >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock
    
self halt.
    
^trueAlternativeBlock value

もし、こいつが本当にコールされていたら、この時点でアウトなはずですが何も起こりませんね。念のため、明示的に True>>#ifTrue:ifFalse: をコールするコードを評価してみます。

true ifTrue: [#true] ifFalse: [#false]   "=> #true "


ふつうに #true を返してきます。がーん。


つまるところ、#ifTrue:ifFalse: をコールするメッセージ式を見つけると、コンパイラはバイトコードレベルで単なる条件分岐のコードにインライン展開してしまう仕組みがあるようです。

ちなみに今回定義した Integer>>#ifTrue:ifFalse: も、True や False の #ifTrue:ifFalse: もメソッド自体は存在しているので、動的、あるいは静的にコールすることで実行は可能です。

0 perform: #ifTrue:ifFalse: with: [#nonZero] with: [#zero] "動的コール"
(Integer >> #ifTrue:ifFalse:) valueWithReceiver: 0 arguments: {[#nonZero]. [#zero]} "静的コール"


先ほど Integer>>#ifTrue:ifFalse: に仕掛けておいた self halt. があるのでノーティファイアが出ますが、これを削除して再コンパイルした後に print it するか、あるいはそのまま Proceed すると、#zero を返してくるはずです。


なお、#ifTrue:ifFalse: のようにバイトコードレベルでインライン展開されるメソッドのセレクタは、MethodNode>>#initialize の冒頭で確認できます。

MethodNode >> initialize
    
"MessageNode initialize"
    
MacroSelectors :=
        
#(    ifTrue: ifFalse: ifTrue:ifFalse: ifFalse:ifTrue:
            
and: or:
            
whileFalse: whileTrue: whileFalse whileTrue
            
to:flag_do: to:flag_by:do:
            
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:)


以上、Smalltalk を例にメッセージングを解説するときの落とし穴でした。



あと、バイトコードを扱ったので、念のためにもうひとつ。

くだんのコメントで指摘した「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]]