はじめに
メリークリスマス! そろそろお休みに入り、おうちでパーティなども良いものですね。
皆で集まってJengaとか、盛り上がるのではないでしょうか。
とはいえ我々プログラマとしては、やはりJengaも大好きなプログラミング言語で行いたいもの。そう、そこでSmalltalk Jengaです。
ただし今回はオリジナルのSmalltalk Jengaとは少し違う形を提案してみます。第129回Smalltalk勉強会の雑談にあがっていた「インスタンス版Smalltalk Jenga」の実装です。
インスタンス版Smalltalk Jengaとは?
通常のSmalltalk Jengaは「クラス」をJengaのブロックに見立てます。つまりSmalltalkシステムから__任意のクラスを選んで削除__していくことで、__「積んであるブロックを抜く」__という行為を表現します。末端のどうでも良いクラスであれば、削除してもシステムは動き続けますが、ArrayやUserInputEventなど、クラス階層の上のほうにある重要なクラスを削除してしまうと、致命的なエラーとなって、システムが瓦解します(フリーズしたり、パシャーンとVMごと落ちたりします)。
このオリジナルの方式でSmalltalkを壊せるので、Jengaとしては十分成立しています。しかし、リアルなJengaで大切な動きである、__「抜き取ったブロックを上に重ねる」__というところまでは表現できていません。削除でなく、選択したクラスをクラス階層の適当な所に挿入するというやり方も考えられますが、実装が多少複雑になりエレガントさに欠けます。よりシンプルにSmalltalk的ミニマルさで表現できる方法はないのでしょうか?
そこでインスタンス版Smalltalk Jengaです。インスタンス版ではSmalltalkで特徴的なメソッドであるbecome:を使います。
become: とは?
Smalltalk流のオブジェクトの定義は
- アイデンティティを持つ
- メッセージを受け取れる
- メッセージを送信できる
と思っているのですが、このうちの、__アイデンティティを別のオブジェクトと取り替え__てしまうというのがbecome:です。
つまりAさん become: Bさんとすると、AさんがBさんになり、BさんがAさんになります。
非常に強力な機能で、下手に使用すると本当に訳のわからないプログラムを書くことができます。
もう少しおとなしいbecomeForward:というものもあり、こちらは一方通行です。AさんがBさんになるだけで、BさんはもとのままのBさんです。
becomeForward:の用法としては、
-
Proxyとして動作していたオブジェクトが、必要に応じて本物のオブジェクトに成り代わる -
ByteArrayであったオブジェクトが、データをコピーしないままStringに変化して、文字の集合としての扱いを可能にする
などがあります。通常は副作用の少ないbecomeForward:のほうを使い、become:の出番はほぼないのですが、Jengaの「抜き取ったブロックを上に重ねる」を表現するには、become:がむしろ適していると言えるでしょう。Smalltalkを25年以上使っていて、ようやく__become:の実用例にたどり着いた__というわけです。
jengaメソッドの中身
任意のオブジェクトからJengaを開始できるようにするには、Objectクラスにjengaメソッドを定義するのが良さそうです。
実装は非常にシンプルで、メモリ上にある適当なクラスの適当なインスタンスを取得して、それに対してbecome:を行うというものです。
Object >> jenga
jenga
Smalltalk allClasses atRandom someInstance ifNotNil: [:someInstance |
Transcript crShow: 'JENGA! ', someInstance printString.
self become: someInstance.
]
Smalltalkのプログラムは左から右に英語っぽく読めるのでなんとなくわかりますね。
Smalltalkが、グローバルな名前空間を提供するシステム辞書です。そこからallClassesですべてのクラスを取り出し、atRandomで適当なクラス、さらにsomeInstanceでそのクラスの適当なインスタンスを取得します。
残念ながらクラスによっては、生成済みのインスタンスがその時に存在しなかったり、そもそも抽象クラスだったりして、someInstanceがnilになるときが結構な頻度であります。
nilもオブジェクトなので、become:できないことはないのですが、仮に実行されたとすると、大ダメージとなってSmalltalkが__VMごと落ちてしまいます__。
メモリ上のすべての初期化されていない変数が、nilの代わりにselfを参照することになり、さらにnilがselfが指していたオブジェクトになるのですから、無理もありません。
これではあまりによく落ちるハードなゲームになってしまうため、ここではifNotNil:を使い、nil以外の場合にのみ、self become: someInstanceが実行されるようにしています。
jengaメソッドの定義
以後の作業はオープンソースのSmalltalk処理系の定番Pharoで行いましょう。Pharo Launcherかpharo-uiのスクリプトでpharoを起動させたら、PlaygroundをControl+owで開きます。(MacではCommandです。適宜読み替えてください。)
Playgroundに
Object browse.
と打ち込んで"Do it"します。(Control+dがショートカットです。)
これでシステムブラウザが開きます。Objectクラスが選択された状態になっています。
左から右にパッケージ、クラス、プロトコル、メソッドの階層となっています。下半分がコードエディタです。
左から3番目のプロトコルペインのリスト上にある'instance side'の項目を選ぶと、コードエディタがメソッド定義のテンプレートを表示します。
messageSelectorAndArgumentNames
"comment stating purpose of message"
| temporary variable names |
statements
そこに上記のjengaメソッドを貼り付けて"Accept"(Control+s)します。
初回なので開発者の名前を聞いてきます。適当に入れておきましょう。
プロトコルを指定していないので、メソッドが'as yet unclassified'に分類されます。
気になる方は右クリックでメニューを出し、"Rename"で'jenga'などにリネームしておくと良いでしょう。
案の定become:を使っている箇所で'Sends "questionable" message'と警告が出ていますが、危険をわかってやっていることなので、気にしてはいけません。
これでjengaメソッドができました。今後システムを何回も壊すことになるので、"Do it"前に環境全体を保存しておくのが安全です。
デスクトップの左クリックでWorldメニューを出し、"Save as..."で'jenga'などの適当な名前をつけてイメージを保存しましょう。
動作確認
ではPlaygroundでメッセージを送信してみます。Objectクラスに定義したので、任意のオブジェクトにjengaを送ることができます。
まずは'Smalltalk'という文字列にjengaを送ってみます。
'Smalltalk' jenga
シングルクォートを入れないとシステム辞書Smalltalkにjengaメッセージを送ることになるので注意しましょう。間違いなく一発で壊れます。
become:の結果を見るために、Control+otでTranscriptも開いておきます。
では"Do it"していきましょう。何回かControl+dを連打してみてください。
数回ほどbecome:した後で、システムがフリーズしました。もう少し頑張ってほしいものですね。
壊れ方にはいくつかのパターンがありますが、基本フリーズすると回復の手段はないので、PharoをVMのプロセスごとkillしましょう。イメージを保存してあれば問題はありません。
毎回"Do it"するのも面倒なので、次はPlaygroundに以下のように書くことにします。
SharedRandom initialize.
someObj := 'Smalltalk'.
counter := 0.
[
[Transcript crShow: (counter := counter + 1).
2 seconds wait.
someObj jenga] repeat.
] fork
SharedRandom initialize.は乱数の生成が毎回同じパターンにならないようにするためのものです。
後はそれほど難しいところはないと思います。
ループのたびにcounterをインクリメントしてTranscriptに表示するようにしています。2 seconds wait.で2秒ほど待った後、jengaをsomeObjに送っています。someObjはbecome:が成功するたびに何かにどんどん変わっていきます。repeatのループから脱出するコードは書いていませんが、そのうちに固まるか落ちるので問題にはなりません。ループ処理全体をforkさせているのは、"Do it"後に同一スレッドでループが始まり、画面の更新が行われなくなるのを防ぐためです。2秒待つ処理は本来は不要なものですが、突然落ちて回数を見失うことがないように入れています。ドキドキ感の演出になっているかもしれません。
JENGA! JENGA!
ではコードの全体を選んで"Do it"しましょう。いろいろな形でフリーズしたり、落ちたりします。みなさんもぜひチャレンジしてみてください! ちなみに私は何度かトライして78までcounterを上げることができました。運良くスクリーンショットも取れたので載せておきます。
上の画像ではわかりませんが、Pharoがフリーズしています。pharo-uiのコンソール画面には
Instance of ZnNewLineWriterStream did not understand #extent
とエラーが出続けていて、ZnNewLineWriterStreamが画面上のUIを構成しているオブジェクト(Morph)に成り代わってしまったことが伝わってきます。MorphicUIManagerがextentで画面上の表示サイズを測ろうとしていますが、もはや相手は改行を出力するためのStreamなのでextentに答える術はありません。
おわりに
この記事を書いていて、まだSmalltalkを習いたてのころ、「壊すのを恐れてはいけない」と言われたのを思い出しました。__すべてをオープンにさらけ出しているのが、Smalltalk__であり、ユーザ、開発者、プログラミング言語開発者との間に垣根がありません。皆が同じくメッセージ送信により、何でも変容させていくことができます。
システムを壊すのも簡単ですが、反面、言語機能を柔軟に拡張させていくことも可能です。
この自由さが私がSmalltalkを使い続けている理由なのかもしれません。
昔エイプリルフール向けに作成したジョークな処理系SmallTalk R4.1は、そうした言語拡張の例になっています。R4.1では他のプログラミング言語向けにSmallTalk R4.1チャレンジというものを提示しました。今回のインスタンス版Jengaもこのチャレンジに加えたいと思います。
いろいろなプログラミング言語でJengaをどのように実装したら良いのか、冬休みにじっくりと考えてみるのも楽しいでしょう。








