アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。
今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームを Smalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita
各要素へのアクセスに使える each
変数
前回、Smalltalk-72 の重複を許さない使い方もできる動的配列 obset
と通常の配列 vector
には map
があるものの、使い方が統一されていないことや、前者の map
では処理中で各要素にアクセスするために内部表現である vec[i]
を使うことなどに触れました。
各要素へのアクセスに用いる vec[i]
については、Smalltalk-72 Instruction Manual には each
も使えるという記述が見つかりますが、これは少し後のバージョンで実装された機能のようです。
この each
を今の May30版でもなんとか使えるようにできないものかと考えてみました。
駄目なやり方 その1:Smalltalkエディタ(構造化エディタ edit
)などを使って map
にただ ☞ each ← vec[i].
を挿入する
edit obset
とすると、Smalltalkエディタを開いてメソッド定義を編集できます。
Enter で ᗉmap ⇒ ()
の ()
に潜って中味を表示、さらにfor
アクションの最後の引数の ()
に Enter してから、input eval
の直前に ☞each ← vec[i].
(コピペ&転送用には "each _ vec[i].
)を Insert して Exit すれば完了です。
この方法は一見うまく動いているように見えますが、クラス内で宣言無しに使用している each
が obset
の外側(多くの場合はトップ)に漏れてしまって駄目です。
駄目なやり方 その2:obset
クラスを再定義する
ALLDEFS に obset
のソースコードがあるんだから、これを修正して再定義すればいいじゃない…と、特に Smalltalk や Ruby に慣れたかたはそう思われるかもしれません。
ところが残念ながら、今の Smalltalk と違い、Smalltalk-72 のクラスは再定義できません。一見できそうですが、それだと追従できないインスタンスが動作不全に陥り、システムで常時使われている obset
などは再定義が終わるか終わらないかのうちにシステムがバグります。
to obset i input each : vec size end ( %add?((size="end_end+1?("vec_vec[1 to "size_size+10])) vec[end]_:) %_?(0=vec[1 to end] find first :input? (SELF add input)) %delete?(0="i_vec[1 to end] find first :input?(!false) vec[i to end]_vec[i+1 to end+1]. "end_end - 1) %unadd?("input_vec[end]. vec[end]_nil. "end_end - 1. !input) %vec?(!vec[1 to end]) %map?(:input. for i _ end to 1 by -1 ("each _ vec[i]. input eval)) %print?(SELF map "(vec[i] print. sp)) %is?(ISIT eval) isnew?("end_0. "vec_vector "size_4) )
デバッガーの無限起動は escキーで止められますが、それ以降、正常な動作は望めないので、Show Nova ボタンを押して画面を切り換えてから、Restart ボタンを押し、Show Smalltalk でまっさらな状態に復帰してください。
Smalltalk の動的性の象徴ともいえる、こういったクラスの根幹を変える変更にも何食わぬ顔で対応できるようになるのは、ポインタのすげ替えを可能にする become:
により作り直したインスタンスを置き換える力業が導入された Smalltalk-76 以降なのです。
駄目な方法 その3:Smalltalkエディタの title
オプションを使う
Smlltalk-72 Instruction Manual を読み進めると、Smalltalkエディタは起動時に title
オプションを付けることで、メソッド本体だけでなくクラス名や各種変数の宣言を(通常のメソッド編集と同様に)動的に変更できそうだと期待させられます。
ですが、これも駄目です。やってみると分かりますが、クラスの再定義と結果は一緒で、each
を :
の前に Insert してから Exit すると同時に画面がバグります。復帰方法はすでに書いた通りです。
edit
のソースを読むと分かりますが、メソッドは編集後のベクターをただ eval
しているだけなので新たにクラスを定義し直すのとまったく変わりませんね。
少しマシな方法: クラス変数に each
を追加して map
に ☞each ← vec[i].
を追加する
前述のとおり、Smalltalk-72 のクラスは、テンポラリ変数やインスタンス変数を追加することはできませんが、クラス変数は比較的自由に追加することが可能です。クラス変数を追加したり、既存のそれらの内容を変更するには PUT
アクションを使用します。
たとえば each
というクラス変数を初期値 nil
で追加するには PUT obset ☞each nil
(コピペ&転送用には PUT obset "each nil
)を評価します。
これなら each
が漏れ出すこともありません。
ところで map
のブロック代わりの引数は vector
のリテラル ☞( ... )
で渡すのですが、for
アクション for i to 5 do (i print. cr).
のように括弧のみで書ける方が見た目がすっきりしますよね。for
アクションでは最後の引数をリファレンスとしてフェッチ :#exp
することでこれを実現しています。
そんなわけで、ここでは次に紹介する方法が使えない vector
に、each
が使えて、引数にクオートアクション ☞
が不要な do
を定義します。(図には obset
に適用した場合の様子も示していますが、ここでは行わないでください。)
PUT vector "each nil addto vector "(%do ? (:#y. for x to SELF length ("each _ SELF[x]. y eval))) "(1 2 3) do (each print. cr).
少しひねりを入れた方法:クラス変数 each
に each
アクションを代入する
この方法は obset
にしか使えないのですが、新しく定義する do
はもちろん、コード変更無しで既存の map
にも each
が利用可能になるワザです。
先の vector
の例と同様に、新しく定義する do
では、obset
の逆順になる謎仕様も昇順に変更しています。
to t each (ev) t to each (!vec[i]) PUT obset "each #each done addto obset "(%do ? (:#input. for i to end (input eval)) "set _ obset. set _ 1. set _ 2. set _ 2. set _ 3. set do (each print. cr). set map "(each print. cr).
おおよそ次のような手順のことをやっています。
まず、アクション(インスタンスを生成しないクラス)t
を定義して、そこにこれから定義するアクション each
と同名のテンポラリ変数を宣言しておきます。アクションのメソッド本体は ev
つまり REPLです。なお t
は特にクラスなどのテンポラリな名前に Smalltalk-72で慣習的に使われます。
次に、アクションt
をコールして t
のコンテキストで REPLを動かします。そこで今回必要になるアクション each
を定義します。通常、アクションto
へのメッセージ送信にによるアクションを含むクラスの定義は、クラス名がトップ(実体は USER
アクションのクラス変数)に追加されますが、t
にはテンポラリ変数 each
が宣言されているので、ここで漏れ出しが食い止められます。
そして PUT
アクションで obset
のクラス変数 each
を追加して、そこに今定義したアクション each
を代入します。
最後に done
で REPLを抜けて元の REPLに戻っています。あとは、addto
アクションで do
メソッドセクションを obset
のメソッドに追加して終わりです。
いろいろ便利なので、以降はこの do
と each
変数を積極的に使ってゆこうと思います。
とりあえず、前回の spacewar
はこのように書き換えられます。
to spacewar x y z : : objects ( (null objects ? ("objects _ obset)) %schedule ? (objects _ :#) %delete ? (%all ? ("objects _ nil) objects delete :#) %run ? (repeat (objects do ("x _ each. x))) )
( 宇宙船の残像を消す へ続く)