1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

インスタンス版Smalltalk Jengaで危険なメリークリスマス

はじめに

メリークリスマス! そろそろお休みに入り、おうちでパーティなども良いものですね。
皆で集まってJengaとか、盛り上がるのではないでしょうか。

とはいえ我々プログラマとしては、やはりJengaも大好きなプログラミング言語で行いたいもの。そう、そこでSmalltalk Jengaです。

ただし今回はオリジナルのSmalltalk Jengaとは少し違う形を提案してみます。第129回Smalltalk勉強会の雑談にあがっていた「インスタンス版Smalltalk Jenga」の実装です。

インスタンス版Smalltalk Jengaとは?

通常のSmalltalk Jengaは「クラス」をJengaのブロックに見立てます。つまりSmalltalkシステムから任意のクラスを選んで削除していくことで、「積んであるブロックを抜く」という行為を表現します。末端のどうでも良いクラスであれば、削除してもシステムは動き続けますが、ArrayUserInputEventなど、クラス階層の上のほうにある重要なクラスを削除してしまうと、致命的なエラーとなって、システムが瓦解します(フリーズしたり、パシャーンと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でそのクラスの適当なインスタンスを取得します。

残念ながらクラスによっては、生成済みのインスタンスがその時に存在しなかったり、そもそも抽象クラスだったりして、someInstancenilになるときが結構な頻度であります。

nilもオブジェクトなので、become:できないことはないのですが、仮に実行されたとすると、大ダメージとなってSmalltalkがVMごと落ちてしまいます
メモリ上のすべての初期化されていない変数が、nilの代わりにselfを参照することになり、さらにnilselfが指していたオブジェクトになるのですから、無理もありません。

これではあまりによく落ちるハードなゲームになってしまうため、ここでは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クラスが選択された状態になっています。

browse-object.png

左から右にパッケージ、クラス、プロトコル、メソッドの階層となっています。下半分がコードエディタです。
左から3番目のプロトコルペインのリスト上にある'instance side'の項目を選ぶと、コードエディタがメソッド定義のテンプレートを表示します。


messageSelectorAndArgumentNames
    "comment stating purpose of message"

    | temporary variable names |
    statements

そこに上記のjengaメソッドを貼り付けて"Accept"(Control+s)します。

defining-jenga-method.png

初回なので開発者の名前を聞いてきます。適当に入れておきましょう。

jenga-method.png

プロトコルを指定していないので、メソッドが'as yet unclassified'に分類されます。
気になる方は右クリックでメニューを出し、"Rename"で'jenga'などにリネームしておくと良いでしょう。

renaming-protocol.png

案の定become:を使っている箇所で'Sends "questionable" message'と警告が出ていますが、危険をわかってやっていることなので、気にしてはいけません。

questionable-message.png

これでjengaメソッドができました。今後システムを何回も壊すことになるので、"Do it"前に環境全体を保存しておくのが安全です。

デスクトップの左クリックでWorldメニューを出し、"Save as..."で'jenga'などの適当な名前をつけてイメージを保存しましょう。

save.png

動作確認

ではPlaygroundでメッセージを送信してみます。Objectクラスに定義したので、任意のオブジェクトにjengaを送ることができます。

まずは'Smalltalk'という文字列にjengaを送ってみます。

'Smalltalk' jenga

シングルクォートを入れないとシステム辞書Smalltalkjengaメッセージを送ることになるので注意しましょう。間違いなく一発で壊れます。

become:の結果を見るために、Control+otでTranscriptも開いておきます。

では"Do it"していきましょう。何回かControl+dを連打してみてください。

jenga-interactive.png

数回ほど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秒ほど待った後、jengasomeObjに送っています。someObjbecome:が成功するたびに何かにどんどん変わっていきます。repeatのループから脱出するコードは書いていませんが、そのうちに固まるか落ちるので問題にはなりません。ループ処理全体をforkさせているのは、"Do it"後に同一スレッドでループが始まり、画面の更新が行われなくなるのを防ぐためです。2秒待つ処理は本来は不要なものですが、突然落ちて回数を見失うことがないように入れています。ドキドキ感の演出になっているかもしれません。

JENGA! JENGA!

ではコードの全体を選んで"Do it"しましょう。いろいろな形でフリーズしたり、落ちたりします。みなさんもぜひチャレンジしてみてください! ちなみに私は何度かトライして78までcounterを上げることができました。運良くスクリーンショットも取れたので載せておきます。

jenga78.png

上の画像ではわかりませんが、Pharoがフリーズしています。pharo-uiのコンソール画面には
Instance of ZnNewLineWriterStream did not understand #extent
とエラーが出続けていて、ZnNewLineWriterStreamが画面上のUIを構成しているオブジェクト(Morph)に成り代わってしまったことが伝わってきます。MorphicUIManagerextentで画面上の表示サイズを測ろうとしていますが、もはや相手は改行を出力するためのStreamなのでextentに答える術はありません。

pharo-ui-console.png

おわりに

この記事を書いていて、まだSmalltalkを習いたてのころ、「壊すのを恐れてはいけない」と言われたのを思い出しました。すべてをオープンにさらけ出しているのが、Smalltalkであり、ユーザ、開発者、プログラミング言語開発者との間に垣根がありません。皆が同じくメッセージ送信により、何でも変容させていくことができます。

システムを壊すのも簡単ですが、反面、言語機能を柔軟に拡張させていくことも可能です。
この自由さが私がSmalltalkを使い続けている理由なのかもしれません。

昔エイプリルフール向けに作成したジョークな処理系SmallTalk R4.1は、そうした言語拡張の例になっています。R4.1では他のプログラミング言語向けにSmallTalk R4.1チャレンジというものを提示しました。今回のインスタンス版Jengaもこのチャレンジに加えたいと思います。

いろいろなプログラミング言語でJengaをどのように実装したら良いのか、冬休みにじっくりと考えてみるのも楽しいでしょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?