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

# はじめに

メリークリスマス! そろそろお休みに入り、おうちでパーティなども良いものですね。
皆で集まって[Jenga](https://ja.wikipedia.org/wiki/%E3%82%B8%E3%82%A7%E3%83%B3%E3%82%AC)とか、盛り上がるのではないでしょうか。

とはいえ我々プログラマとしては、やはりJengaも大好きなプログラミング言語で行いたいもの。そう、そこで[Smalltalk Jenga](https://qiita.com/kueda_sorabito/items/a07b67822c686ac8a344)です。

ただし今回は[オリジナルのSmalltalk Jenga](https://qiita.com/kueda_sorabito/items/a07b67822c686ac8a344)とは少し違う形を提案してみます。[第129回Smalltalk勉強会](https://www.smalltalk-users.jp/Home/gao-zhi/dai129kaismalltalkbenkyoukai)の雑談にあがっていた「インスタンス版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

```smalltalk
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](https://pharo.org/)で行いましょう。Pharo Launcherか`pharo-ui`のスクリプトでpharoを起動させたら、PlaygroundをControl+owで開きます。(MacではCommandです。適宜読み替えてください。)

Playgroundに

```
Object browse.
```

と打ち込んで"Do it"します。(Control+dがショートカットです。)

これでシステムブラウザが開きます。`Object`クラスが選択された状態になっています。

![browse-object.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/5b501556-f2c2-5ae9-04b4-f962f51453f3.png)

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

```smalltalk

messageSelectorAndArgumentNames
	"comment stating purpose of message"

	| temporary variable names |
	statements
```

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

![defining-jenga-method.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/e0f3387b-63cc-01d4-a2bd-690e923cc034.png)

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

![jenga-method.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/b701b653-5f33-3033-ee9f-6669281b6b66.png)

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

![renaming-protocol.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/84694af6-b2ed-cd6a-66bf-f3b154146a3a.png)

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

![questionable-message.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/16db5cb4-e047-dcc1-85e6-60610c8e57e6.png)

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

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

![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/5d1e94ca-8ab8-3c74-2d01-fb8be1d37493.png)

# 動作確認

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

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

```smalltalk
'Smalltalk' jenga
```

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

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

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

![jenga-interactive.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/3c90df83-3db7-aafb-911e-d4b033d87006.png)

数回ほど`become:`した後で、システムがフリーズしました。もう少し頑張ってほしいものですね。
壊れ方にはいくつかのパターンがありますが、基本フリーズすると回復の手段はないので、PharoをVMのプロセスごとkillしましょう。イメージを保存してあれば問題はありません。

毎回"Do it"するのも面倒なので、次はPlaygroundに以下のように書くことにします。

```smalltalk
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`を上げることができました。運良くスクリーンショットも取れたので載せておきます。

![jenga78.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/8e24f74d-1937-dfbb-e293-395283153317.png)

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

![pharo-ui-console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61787/1c224c16-1483-c2c3-1cd1-8c70bb8d4858.png)

# おわりに

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

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

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

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