Smalltalk-76 と Squeak/Pharo で PPAP ~40年の時空を超えた世代間 Smalltalk 比較~


はじめに

遅まきながら PPAPズンドコキヨシ を Smalltalk で書いてみました。仕様はいろいろと考えられそうですが、ここでは、

  1. ペンとアップルとパイナップルをランダムで選んで、I have a ~ と歌わせて、
  2. ペンに続いてアップルが来たら、ah! Apple pen!
  3. ペンに続いてパイナップルが来たら、ah! Pineapple pen!
  4. ペン、アップル、ペン、パイナップルの順で並んだら、Apple pen! Pineapple pen!、ah! Pen Pineapple Apple pen! で締めて終わり

というものにしました。おまけとして、Apple のときだけ不定冠詞を an に変えます。

まずは Squeak/Pharo で PPAP

いつものスクリプト風で、Squeak/Pharo どちらでも動くコードにしました。Squeak なら Workspace、Pharo なら Playground に貼り付け(ctrl + v)て、全選択(ctrl + a) 後、do it (ctrl + d) で評価できます。

| ppap sequence applePen pineapplePen |
ppap := #(Pen Apple Pen Pineapple) collect: [:sym |
    Smalltalk at: sym ifAbsent: [Object newSubclass rename: sym]
].
applePen := ppap first: 2.
pineapplePen := ppap last: 2.
sequence := OrderedCollection new: 4 withAll: nil.
(Smalltalk version beginsWith: 'Squeak') ifTrue: [Transcript perform: #closeAllViews].
Transcript open.

[(sequence last: 4) asArray = ppap] whileFalse: [
    Transcript cr; show: 'I have ', (sequence add: ppap atRandom) new printString.
    (sequence last: 2) asArray caseOf: {
        [applePen] -> [Transcript cr; show: 'ah!  Apple pen!'].
        [pineapplePen] -> [Transcript cr; show: 'ah!  Pineapple pen!']
    } otherwise: [].
].

Transcript cr; show: 'Apple pen!  Pineapple pen!\ah!  Pen Pineapple Apple pen' withCRs

以下、何をやっているのか順を追って解説します。

まず、おまけの不定冠詞(a/an)の切り替えですが、これは Smalltalk のオブジェクトを文字列で表現すると、リテラルやそれに準ずる生成式を持たないオブジェクトの場合、デフォルトで「不定冠詞+クラス名」になる機構をそのまま流用したので、特に細工はしていません。

Object new printString  "=> an Object "

そんなわけで、とりあえず、ペン(Pen)、アップル(Apple)、パイナップル(Pineapple)をそれぞれクラスとして定義し、まとめて ppap と名付けた配列に収めています。

ppap := #(Pen Apple Pen Pineapple) collect: [:sym |
   Smalltalk at: sym ifAbsent: [Object newSubclass rename: sym]
].

Pen → Apple、Pen → Pineapple の順で選ばれたときの判定用に、ppep の最初の2つで applePen、残りの2つで pineapplePen も作っておきます。

applePen := ppap first: 2.
pineapplePen := ppap last: 2.

ループ内では、ppap の中からランダムで選んだクラスのインスタンスを生成し、それを「I have 」の後に続けて文字列表現で出力することで簡易に処理します。

Transcript cr; show: 'I have ', (sequence add: ppap atRandom) new printString.

ランダムに選んだクラスは、sequence に都度 #add: していき、ループ脱出の判定、もしくは続く並びの判定に用います。

sequence の記録の最後の2つについて、あらかじめ用意した applePen もしくは pineapplePen と比較することで Pen → Apple もしくは Pen → Pineapple の並びと判断します。Smalltalk では #ifTrue:ifFalse: のネストで多段条件分岐を表現するのですが、Squeak/Pharo 系には Apple Smalltalk 時代に追加された独自の #caseOf:otherwise: があるので、これを使いました。

(sequence last: 2) asArray caseOf: {
    [applePen] -> [Transcript cr; show: 'ah!  Apple pen!'].
    [pineapplePen] -> [Transcript cr; show: 'ah!  Pineapple pen!']
} otherwise: [].

Pen → Apple → Pen → Pineapple の順で選ばれてループを抜けたら、締めの出力をしておしまいです。

Alto と Smaltalk-76 について

Alto は 1973年にアラン・ケイらの要請で天才チャック・サッカーにより製作されたパーソナルコンピューター試作機です。当時のミニコン並みのパワーを持ち、かつ、パソコンが そのようなパワーを有するようになるだろうと予想できる 10~15年先を見越したスペックで、1970年代を通じて3モデル 2000台弱が作られ、ゼロックス社のパロアルト研究所(Palo Alto Research Center, PARC)内外で各種研究や実験に用いられました。一方で Alto は、当初の目的であるパソコンとしてよりは、その後大幅にパワーアップされた後継の Dolphin、Dorado、Dandelion(後の Star)を通じてワークステーションの始祖的な存在としてもよく語られます。ただ Alto 自体の処理能力は(しかし前述の設計時点での想定のとおり)1986 年の Mac Plus 程度だったと言われています。

アラン・ケイらの「暫定ダイナブック」もこの Alto を使った数あるプロジェクトの成果物の中の一つです。最終的には GUI 付きの仮想 OS として Smalltalk-76 を動作させることにより、当時から見て近い将来のパーソナルコンピューターのあり方を(スティーブ・ジョブズらの理解不足から GUI というほんの表層的な部分に限定されはしたものの)世に示し、文字通り「未来を創る」ことに成功したわけです。

Alto というと、最近では Y-Combinator による現存する実機を使った復元プロジェクトが話題になりましたが、ソフトウエアによる復元プロジェクトも近年急速に進展しています。そのひとつが Living Computer Museum により C#/.NET でつくられた ContrAlto: A Xerox Alto Emulator です。ここではこの ContrAlto で ジョブズたちも見たであろう当時のままの Smalltalk-76 を動かして PPAP に挑戦します。

Smalltalk-76 は、メッセージングというアイデアをドグマにして考案された Smalltalk-72 の後に作られた Smalltalk で、その後 BYTE紙の特集号などで広く知られるようになった XEROX Smalltalk-80(VisualWorksPharoSqueak といった今の Smalltalk 処理系の直系の先祖)のさらに前身の言語・環境です。

Smalltalk-72 が文法らしい文法を持たず、関数のようなクラスにリーダーマクロでメッセージの処理を記述したかなり変わった言語であったのとは異なり、Smalltalk-76 は現在の多くの OOP と同様に SIMULA-67 タイプのクラスやメソッドを持ちます。また、後に Smalltalk の特徴とされるようになる「メッセージ式」という新しい文法もこのときに取り入れられました。一方で、for、while、until といったループ構文があったり、メタクラスや第一級オブジェクトとしてのブロックがまだないなど Ruby ともなんとなく似た特徴も持ち合わせるおもしろい言語です。

ContrAlto で Smalltalk-76 起動と基本操作

ContrAlto で Smalltalk を起動するには、こちらの記事が参考になります。

Simulating a Xerox Alto with the ContrAlto simulator: games and Smalltalk

To start up Smalltalk, download st80.dsk. Load the disk image into ContrAlto, reset, and then run "resume small.boot". (A second Smalltalk disk is xmsmall.zip; "resume xmsmall.boot" starts Smalltalk from this disk.). I should mention that the Smalltalk system is fairly slow, so be patient.

私は xmsmall.zip の方のディスクイメージ(xmsmall.dsk)を Drive 0 に Load... し、Start(もしくは Reset)後、コンソールから resume xmsmall.boot で Smalltalk-76 環境をブートしました。

ブート直後はウインドウの中身は表示されていない状態ですが、どちらかのウインドウにマウスカーソルを移動して重ねたり、もう一方は明示的にクリック(反応が鈍いので“クリック”と言うよりは“長押し”)することでアクティベートすると、中身が描かれます。

ContrAlto を使うのには必ずスクロールボタンが押下可能な3ボタンマウスを用いてください。Smalltalk-76 では、中ボタン(スクロールボタン)が Windows などでいうところの右クリックに相当し、その代わり右ボタンはウインドウ操作のための第三ボタンとして機能します。ウインドウの中で右クリックすると、ウインドウ操作用のメニューがポップアップするので、移動する場合は frame を選んで移動先の矩形を左ボタンのドラッグで描いて示します。

ウインドウを閉じるには同じ右クリックメニューから close を選択します。

なお、左上の矩形は Dispframe と呼ばれる REPL あるいは CUIコンソールで、ウインドウではないため(アクティベートを除き)通常のマウス操作は受け付けないので注意が必要です。REPL として使うには(クリックしてアクティベート後に)評価したい式をタイプして入力し、キーボードの下方向カーソル移動キーを押下します。

ちなみに、Smalltalk-76 環境では上下左右方向のカーソル移動キーでキャレットの移動はできないので、かならずマウスクリック(くどいようですが ContrAlto では長押し^^;)で移動させる必要があります。

デスクトップで中ボタンクリックするとメインメニューがポップアップするので、open a subview で仮想デスクトップのポータル(見た目は単なるウインドウ。中ボタンクリック → enter で仮想デスクトップに移動できる。元のデスクトップに戻るにはメインメニューの exit to overview )の作成、open a browser でシステムブラウザ(クラスブラウザ)の起動、open a workspace でワークスペース(メモ帳。実体は、UserView>>#workspace のコード編集枠。コメント内に記述したコードやテキストを中ボタンクリック → compile で保存が可能)の起動、quit で環境を保存して終了が可能です。reclaim はメモリの回収をするための機能のようです。

1980 年代の Lisa や Mac のルック&フィールを知っている人からするとジョブズの言うようにかなり原始的で未完成な GUI であることは否めませんが、それでも Apple がその後の GUI のルック&フィールのいっさいがっさいを新たに創造したかのように主張したり、印象の操作を試みることは、GUI 史を語る上では適切ではないことがわかります。

Smalltalk-76 で PPAP

例によって前置きがずいぶん長くなってしまいましたが、ここで本題の PPAP です。コードはこんな感じにしてみました。

| ppap sym class pos applepen pineapplepen seq i.

user newdisploc: 8◉20 and: 210◉250; clear; show.

ppap ← ↱(Pen Apple Pen Pineapple) transform▷ sym to▷ [
    class ← Smalltalk lookup: sym.
    class = false ⇒ [Class new title: sym; fields: ''; itself] class
].

pos ← 1 to: ppap length.
applepen ← ppap copy: 1 to: 2.
pineapplepen ← ppap copy: 3 to: 4.
seq ← Queue new of: ↱().
for▷ i to: 4 do▷ [seq push: nil].

until▷ seq contents = ppap do▷ [
    user cr; show: 'I have '.
    seq next; append: (class ← ppap ◦ pos random).
    class new print.

    (seq contents copy: 3 to: 4)
        = applepen ⇒ [user cr; show: 'ah!  Apple pen!'];
        = pineapplepne ⇒ [user cr; show: ''ah!  Pineapple pen!']
].

user
    cr; show: 'Apple pen!  Pineapple pen!';
    cr; show: 'ah!  Pen Pineapple Apple pen!'.


まず、Dispframe が PPAP の出力を担当させるにはちょっと狭いので拡張します。

user newdisploc: 8◉20 and: 210◉250; clear; show.

次に Pen 、Apple、Pineapple クラスを(無ければ)定義します。

ppap ← ↱(Pen Apple Pen Pineapple) transform▷ sym to▷ [
    class ← Smalltalk lookup: sym.
    class = false ⇒ [Class new title: sym; fields: ''; itsself] class
].

Ruby でよく知られるようになった Smalltalk のいわゆる ~ect 系のメソッドは Smalltalk-76 時代にはまだなかったので、#collect: ではなく、それに相当する #transfer▷to▷ というメソッドを使います。↱ と ▷ は実際は半周した矢印と白抜きのコロンで、前者はシンボルや配列を記述する際に(Smalltalk-80 以降では # を使う)、後者は引数を評価せずメソッドに渡すとき(Smalltalk-72 由来で Smalltalk-80 以降にはない仕組み)に使います。代入に ← を使うのは初期の Smalltalk-80 と一緒ですね。一方で、真偽値式 ⇒ [ true時評価式 ] false時評価式 で if-then-else を表わすのは Smalltalk-72 由来です。

Smalltalk-76 にはまだ Random クラスはなく、Interval>>#random を使って指定範囲の乱数を発生させます。

pos ← 1 to: ppap length.
class ← ppap ◦ pos random

◦ は Smalltalk-80 以降の #at: に相当する記号(Array に定義された普通のメソッド)です。Smalltalk-76 では ← もメソッド名に含めることができるので、#◦← というメソッドで、Ruby の array[3] = val よろしく、array ◦ 3 ← val のような代入を模した表現も可能です。

Smalltalk-80 以降の OrderedCollection のような機能は見つけられなかったので、Stream のサブクラスの Queue を使って直近の 4 つの選択肢を保持し、Pen → Apple、Pen → Pineapple 、Pen → Apple → Pen → Pineapple の順かどうかの判断を行ないました。

seq ← Queue new of: ↱().
for▷ i to: 4 do▷ [seq push: nil].

until▷ seq contents = ppap do▷ [
    user cr; show: 'I have '.
    seq next; append: (class ← ppap ◦ pos random).
    class new print.

    (seq contents copy: 3 to: 4)
        = applepen ⇒ [user cr; show: 'ah!  Apple pen!'];
        = pineapplepne ⇒ [user cr; show: ''ah!  Pineapple pen!']
].

Smalltalk-76 はカスケード(;)の対象範囲が Smalltalk-80 以降のそれとは異なるためか、if 式のカスケードがきれいに書け、case 式が不要になっているのが嬉しいですね。

ループを抜けたら締めのメッセージ出力をするくだりは Squeak/Pharo と一緒です。

まとめ

Smalltalk-76 には、当時の GUI としての先進性は言うまでもなく、言語としても、クラスの動的生成が可能であったりそれが第一級オブジェクトであること、ライブラリや API の充実度、すでに不定冠詞の切り替えがデフォで組み込まれる等のきめ細やかさなど、1970年代の言語・環境とは思えない先進ぶりに驚愕させられます。もちろん 40年を経てさらにその間、ユーザー達の手による斬新的な進化を遂げた Squeak/Pharo の優位性は揺るぎのないものですが、Smalltalk-76 の方にもまだ目を見張る優れた点を見いだすことができ、それらが Smalltalk-80 に継承されなかった理由も含め、大いに学びがいはありそうです。

補足:実際に ContrAlto で試してみる場合のヒント

中ボタンクリック → do it ができる場所ならどこでも上のコードを入力し実行可能ですが、デスクトップを中ボタンクリック → open a workspace でワークスペースを新たに開いてそこに入力するのがベターかと思います。とにかく動作は重いので特にマウスボタンの押下は反応を待って長押しを心がけてください。

ワークスペースはデフォルトでは中央揃えになっていて扱いづらいので、中ボタンクリック → align (一番下のメニュー項目。一番上の again ではありません。念のため)を2回選択して左揃えに戻しておくとよいです。

XEROX と書いてあるすぐ上の行の " の右側をダブルクリック(もちろん、長押し+長押し)すると、コメント内の全選択ができるの(選択状態になるのには時間がかかるので注意)で、return キーで改行に置き換えて一括削除し、そこにコードを書くのがよいでしょう。

ちなみに範囲選択後、いきなり delete キーを押すと、直前の文字も削除されてしまうので、いったん delete 以外のキーを押して選択範囲を置き換えてから改めて delete キーを押すのが良いようです。

上のコードで用いている特殊な記号+α は手元の環境(MacBook Air, US キーボード, Boot Camp, Windows 8.1)では次のキーコンビネーションで入力できたので参考にしてください。入力できない場合は、最終手段としてクラスブラウザで適当なメソッドの定義から選択後、中ボタンクリック → copy & paste で持ってくる手もあります。

・座標構築セレクター(上コードでは ◉ )…… ctrl + ] または ctrl + g
・代入(同 ← ) …… 左方向カーソル移動キー
・シンボルおよび配列リテラル記号(同 ↱) …… ctrl + '
・白抜きコロン(同 ▷ ) …… ctrl + ;
・配列・辞書要素アクセスセレクター(同 ◦ ) …… ctrl + [
・if-then(同 ⇒ ) …… ctrl + /
・リターン(⇑) …… ctrl + 上方向カーソル移動キー または ctrl + q
・≦ …… ctrl + , または ctrl + a
・≧ …… ctrl + . または ctrl + r
・≡ …… ctrl + =
・≠ …… ctrl + shift + = または ctrl + n

割り込みは ctrl + c 、デバッガーの起動はノーティファイアの中ボタンクリック → frame です。