#はじめに
ちょっと前に見つけたインディアン・ポーカーの問題
某所で知ったこの問題。単純な割に型をどう定義するか、処理をどこに置くか考えさせられて楽しかった。DDD、DCI向けにも良い例になるかも。条件を変えるとうまく成立しない等あったので勝手ながら一部変更してみた→コーディング試験を受ける | リクルートコミュニケーションズhttps://t.co/BZ9P48Xz9k pic.twitter.com/YPTRwI1KAF
— sumim (@sumim) 2018年9月23日
これを実装するにあたって、どんなクラス設計がよいのかなぁ…などと考えていたとき、ふと“自分の額にかざした(つまり自分は見ることができない)カード”をどのようにプロパティとして表現したらよいのだろう?というのが気になりました。
このとき最初に思いついたのが「プライベートメソッドと逆の振舞い」です。
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 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つの同名メソッドが存在することがわかります。
本来ならきちんと調べないといけないのですが、たぶん(ぉぃぉぃ…) MessageNode>>#pvtCheckForPvtSelector:
でコールしているのは SelectorNode>>#isPvtSelector
の方で、その中でさらにコールされているのが Symbol>>#isPvtSelector
なのでしょう。きっと。^^;
これらをベースに、それぞれ SelectorNode>>#isXvtSelector
と Symbol>>#isXvtSelector
を定義して追加します。
SelectorNode>>#isXvtSelector
は簡単です。最初の行を含めてメソッド中に登場する Pvt
を2つとも Xvt
と書き換えるだけです。
コメントもそれっぽく書き換えたら、alt + s もしくは右クリック → accept (s)
でコンパイルします。
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)
でコンパイルします。
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:
を定義すれば準備は終わりです。
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:
を選び、そのコール元の定義を呼び出します。
▸senders
で pvtCheckForPvtSelector:
のコール元の定義を呼び出す
▸pvtCheckForPvtSelector:
をコールしている MessageNode>>#receiver:selector:arguments:precedence:from:
メソッドの定義
最後に式の区切りであるピリオドを追加し、続く行に self xvtCheckForXvtSelector: encoder
と追加してコンパイルすれば完了です。
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: encoder
を self yourself xvtCheckForXvtSelector: encoder
などとすれば回避できます。もし何かミスをしてコンパイラ自体がうまく動かなくなってしまったら、環境を終了( Squeak メニュー → Quit
)後、再起動して、またチャレンジしてみてください。
次回起動後も機能するように、今回システムに加えた変更を永続化したければ、Quit
ではなく、Save and Quit
などでイメージを保存してから終了すればよいでしょう。