はじめに
メリークリスマス! そろそろお休みに入り、おうちでパーティなども良いものですね。
皆で集まって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をどのように実装したら良いのか、冬休みにじっくりと考えてみるのも楽しいでしょう。