Smalltalk-72で遊ぶOOPの原点:組み込みコレクションにeach変数が使える「do」を新たに追加する

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された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 すれば完了です。

この方法は一見うまく動いているように見えますが、クラス内で宣言無しに使用している eachobset の外側(多くの場合はトップ)に漏れてしまって駄目です。

駄目なやり方 その2:obset クラスを再定義する

ALLDEFS に obsetソースコードがあるんだから、これを修正して再定義すればいいじゃない…と、特に SmalltalkRuby に慣れたかたはそう思われるかもしれません。

ところが残念ながら、今の 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).

少しひねりを入れた方法:クラス変数 eacheach アクションを代入する

この方法は 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 のメソッドに追加して終わりです。

いろいろ便利なので、以降はこの doeach変数を積極的に使ってゆこうと思います。

とりあえず、前回の 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)))
)

宇宙船の残像を消す へ続く)