プライベートメソッドと逆の振る舞いを Squeak Smalltalk のメソッドに追加する


はじめに

 ちょっと前に見つけたインディアン・ポーカーの問題


 これを実装するにあたって、どんなクラス設計がよいのかなぁ…などと考えていたとき、ふと“自分の額にかざした(つまり自分は見ることができない)カード”をどのようにプロパティとして表現したらよいのだろう?というのが気になりました。

 このとき最初に思いついたのが「プライベートメソッドと逆の振舞い」です。

 Pharo Smalltalk からは除かれてしまっていますが、Squeak Smalltalk には pvt で始まるメソッドは self をレシーバーにしないとコールできないというコンパイル時制約が設けられています。Ruby で、レシーバーを省略しないと(ニアリーイコール、レシーバーが self でないと)コールできない private なメソッド属性と似た考え方ですね。

 であれば、これの逆、つまり self をレシーバーにできないメソッドを定義できれば、たとえば額にかざして自分は見てはいけないようなプロパティに誤ってアクセスしようとしても、(間接アクセス・パターン使用時に限りますが─)コンパイルが通らないというようなことが可能になるはずです。

Squeak Smalltalk の入手とインストール、起動

 Squeak Smalltalk処理系は squeak.org から入手できます。2018年12月現在のバージョンは 5.2 です。

 各プラットフォーム向けの .zip(macOS は .dmg)、もしくは主要3プラットフォーム向け VM が同梱された All-in-One.zip をダウンロードして展開(macOS は アプリケーションフォルダにコピー)すればインストールは完了です。

 .image ファイルを、そのプラットフォームの実行形式ファイルである仮想マシン(Windows なら Squeak.exe)にドロップインすれば起動できます。macOS ではコピーした Squeak5.2-nnnnn-64bit.app をそのままダブルクリックで起動できます。

 ちょっと遊ぶだけなら、初回起動時にでるメッセージはスルー(Skip を選ぶか、デスクトップの適当な場所をクリック)でよいでしょう。


Squeak 環境の初回起動時の画面

Squeak Smalltalk のプライベートメソッドの挙動

 改めて、Squeak Smalltalk のプライベートメソッドの挙動(より厳密にはプライベートメソッドをコールするメッセージ式を含むメソッドをコンパイルしたときの挙動)を見てみましょう。

 Squeak Smalltalk で pvt で始まるメッセージを self 以外をレシーバーにしてコンパイルしようとすると、次の一連の図にあるようにインライン警告が出てコンパイルが通りません。


0 pvtMessage という式を評価(print it など)しようとした場合…


pvtMessage メソッドが未定義なので疑問を呈してきますが、別に間違っていないよ!と Choose を選んでコンパイル作業を続行させようとすると…


▸“プライベートメッセージは self がレシーバーじゃないとコンパイルはまかりならん!”と叱られます


▸もとより、self pvtMessage なら何の問題も無くコンパイルは通り、コードは実行されて無事(?) MessageNotUnderstood 例外があがります。

Squeak Smalltalk のプライベートメソッドの実装

 ではこの振る舞いの実装はどうなっているのでしょうか?

 コンパイル時にインラインで表示された警告 Private messages may only be sent to self のうち、特徴的な Private messages の部分を選択して、右クリック → more...method strings with it (E) を選択すると、当該警告を表示する処理と関わりの深いメソッド MessageNode>>pvtCheckForPvtSelector: の定義を呼び出せます。


MessageNode>>#pvtCheckForPvtSelector: の定義

 どうやら、セレクターがプライベート属性を持つ(つまり、pvt で始まる)場合、もしそうなら、レシーバーが擬変数 self でないかについて、抽象構文木からコードを生成する途中でチェックを行ない、これにひっかかれば件のインライン警告を出すしくみのようです。

 従ってこの逆、つまりセレクターが自己排除属性を持つ(仮に xvtで始まる)にも関わらず、レシーバーが self である場合のチェックをし、そうなら警告を出すMessageNode>>#xvtCheckForXvtSelector: を新たに定義すれば目的は達成できそうです。

プライベートメソッドと逆の振る舞いを実装する

 まず無害な isXvtSelector から実装しましょう。

 先ほど呼び出した MessageNode>>#pvtCheckForPvtSelector: の定義を表示しているウインドウにある implementors ボタンをクリックすると、その定義中でコールされているメソッド名(セレクターと言います)の一覧が出てくるので、その中から isPvtSelector を選びます。


▸メソッド中でコールされているメソッドの一覧から isPvtSelector を選ぶ

 すると、2つの同名メソッドが存在することがわかります。


isPvtSelector の実装が2つ見つかる

 本来ならきちんと調べないといけないのですが、たぶん(ぉぃぉぃ…) MessageNode>>#pvtCheckForPvtSelector: でコールしているのは SelectorNode>>#isPvtSelector の方で、その中でさらにコールされているのが Symbol>>#isPvtSelector なのでしょう。きっと。^^;

 これらをベースに、それぞれ SelectorNode>>#isXvtSelectorSymbol>>#isXvtSelector を定義して追加します。

 SelectorNode>>#isXvtSelector は簡単です。最初の行を含めてメソッド中に登場する Pvt を2つとも Xvt と書き換えるだけです。

 コメントもそれっぽく書き換えたら、alt + s もしくは右クリック → accept (s) でコンパイルします。

SelectorNode >>
isXvtSelector
    "Answer if this selector node is a self-exclusive message selector."

    ^key isXvtSelector


SelectorNode>>#isXvtSelector のコンパイル

 コンパイル後、表示は元の SelectorNode>>#isPvtSelector のものに戻ってしまいますが、ちゃんと SelectorNode>>#isXvtSelector は追加されているので安心してください。初回コンパイル時、イニシャルを求められるのでこれも適当に教えてやってください。

 続けて上の枠から Symbol>>#isPvtSelector の定義を選択して表示を切り替え、これも次のように書き換えて alt + s もしくは accept (s) でコンパイルします。

Symbol >>
isXvtSelector
    "Answer whether the receiver is a self-exclusive message selector, that is,
    begins with 'xvt' followed by an uppercase letter, e.g. xvtStringhash."

    ^ (self beginsWith: 'xvt') and: [self size >= 4 and: [(self at: 4) isUppercase]]

 あとは、再び MessageNode>>#pvtCheckForPvtSelector: の定義を表示しているウインドウに戻って、同じような要領で MessageNode>>#xvtCheckForXvtSelector: を定義すれば準備は終わりです。

MessageNode >>
xvtCheckForXvtSelector: encoder
    "If the code being compiled is trying to send a self-exclusive message (e.g. 'xvtCheckForXvtSelector:') to self, then complain to encoder."

    selector isXvtSelector ifTrue:
        [receiver isSelfPseudoVariable ifTrue:
            [encoder notify: 'Self-exclusive messages may only be sent to other than self']].

 このコードをコピペせず、ご自身で編集される場合は、receiver isSelfPseudoVariable のチェックを ifFalse: から ifTrue: に変えておくのをお忘れなく!

 最後に、senders ボタンを押して一覧から pvtCheckForPvtSelector: を選び、そのコール元の定義を呼び出します。


senderspvtCheckForPvtSelector: のコール元の定義を呼び出す


pvtCheckForPvtSelector: をコールしている MessageNode>>#receiver:selector:arguments:precedence:from: メソッドの定義

 最後に式の区切りであるピリオドを追加し、続く行に self xvtCheckForXvtSelector: encoder と追加してコンパイルすれば完了です。

MessageNode >>
receiver: rcvr selector: aSelector arguments: args precedence: p from: encoder 
    "Compile."

    self receiver: rcvr
        arguments: args
        precedence: p.
    originalSelector := aSelector.
    self noteSpecialSelector: aSelector.
    (self transform: encoder)
        ifTrue: 
            [selector isNil ifTrue:
                [selector := SelectorNode new 
                                key: (MacroSelectors at: special)
                                code: #macro]]
        ifFalse: 
            [selector := encoder encodeSelector: aSelector.
            rcvr == NodeSuper ifTrue: [encoder noteSuper]].
    self pvtCheckForPvtSelector: encoder.
    self xvtCheckForXvtSelector: encoder

 動作を検証するには、改行を追加→直後に削除するなどして変更を加えた後、再コンパイル(alt + s もしくは、右クリック → accept (s))してみればよいでしょう。うまく動作しているなら、次図のようにコンパイルは拒否されるはずです。


xvt で始まるメソッドを self をレシーバーにしてコールしようとしているので、インライン警告が出てコンパイルができなくなっている

 どうしてもこのメソッドを再コンパイルしたい場合は、self xvtCheckForXvtSelector: encoderself yourself xvtCheckForXvtSelector: encoder などとすれば回避できます。もし何かミスをしてコンパイラ自体がうまく動かなくなってしまったら、環境を終了( Squeak メニュー → Quit )後、再起動して、またチャレンジしてみてください。

 次回起動後も機能するように、今回システムに加えた変更を永続化したければ、Quit ではなく、Save and Quit などでイメージを保存してから終了すればよいでしょう。