Mini Squeak 2.2 (Webブラウザで動作する、古く、最小構成に近いSmalltalk)の古典的 MVC でタイマーを実装する

前回のエントリー『「使わないと損をする Model-View-Controller」のサンプルコードを SqueakJS のデモ画面(Mini Squeak 2.2)で動かす 』では個人的にも実に久しぶりに古典的MVC に触れて楽しかったので、今回は引き続き Mini Sqeauk 2.2 環境を使って @miwa719 さんにご提案いただいたお題にチャレンジしてみました。

容量を小さくすることを優先して、必要最低限を超えてクラスやメソッドがそぎ落とされてしまった Mini Squeak 2.2.の環境では何かと制約が多いため、なるべくシンプルな実装を目指し、古典的MVC に用意されたプラガブルVCオブジェクト(今のOSで言うウィジェット)をポトペタ感覚で組み合わせてそれっぽく仕上げてみました。

f:id:sumim:20190430171608p:plain

仕様も単純にしています。

  1. min、sec ボタンを押すと、それぞれ分と秒の数値がインクリメントされる。
  2. clear で 00 : 00 にリセット
  3. start でカウントダウンを開始して、00 : 00 になるとビープを決められた回数鳴らす
  4. カウントダウン中(あるいはビープを鳴らしている最中)に clear を押すと止められる
  5. カウントダウン中(あるいはその終了後)、下の枠内に開始時刻と終了時刻、カウントダウンした時間を表示する

以下は、実装時の試行錯誤の過程をさわりだけですが再現してみたものです。最終的に書き上がったコードは最後のリンク先に置いてありますので、前回のエントリー同様、ダウンロード後、ファイルをドラッグ&ドロップで Webブラウザのストアにコピーして、Mini Squeak 2.2環境内のファイラ(デスクトップクリック → open... → file list)から fileIn 後、システムブラウザでコードを見たり、動かして遊んでみたりしてください。

※SqueakJS は AndroidiOSバイスでも動作しますが、外部からのコードの読み込みはちょっと難しいかもしれません(今のところ方法を思いつきません…)。実際に試してはいないのですが、コードを直接タイプして入力すればもしかしたら動くかもしれません。どうぞあしからず。

MacChrome に奪われた簡易補完キーアクションのキーアサインを変更する

SqueakJS を動かす Webブラウザには Chrome を推奨しているのですが、Mac版を試していてまず最初に困ったのがメソッド名(Smalltalkでは「メッセージセレクタ」あるいは短く「セレクタ」と呼ぶ)を簡易的に補完する機能(query symbol, cmd + q)が使えないことでした。

Smalltalk のメソッド名は他言語に比べると長いのが特徴です。たとえば addSubView: hoge toRightOf: fuga なんてのをいちいちタイプするのはおっくうなので addsu くらいまでタイプしたところで addSubView: addSubView: below: addSubView: toRightOf: などから必要なものを選ぶことができる程度の手抜きはしたいものです。これをサポートしてくれるのが前述の cmd + q で利用できる query symbol という簡易補完機能なのですが、よく知られている通り、Mac では Chrome に限らず、そして古くから、同キーコンビネーションは伝統的に Quit に割り当てられているため使えません。そこで、実質空いている cmd + 3 に振り替えます。

本来であれば ParagraphEditor class>>initializeCmdKeyShortcuts(クラスPharagraphEditorのクラスメソッドinitializeCmdKeyShortcutsを慣習的にこう表記する)を修正するのがスジですが、ここは簡単のため ParagaphEditor クラスのクラス変数 CmdActionsアサインされている辞書代わりの配列の内容を半ば強引に直接変更してしまいます。

ワークスペース(デスクトップクリック → open... → workspace で起動)などで次の式をタイプして入力するかコピペしてから念のため選択した後、右クリック → do it (d) します。

 (ParagraphEditor classPool at: #CmdActions) at: $3 asciiValue + 1 put: #querySymbol:

f:id:sumim:20190501143332p:plain

特に画面に変化はないのですが、このあとたとえば 100 fac までタイプして cmd + 3 を押すと factorial と補完され、さらに押すと画面が反転して元の fac に戻るようなら設定の変更は成功です。

f:id:sumim:20190501143730p:plain f:id:sumim:20190501143757p:plain

なお Win版 Chrome で動かす場合、この簡易補完機能は alt + q で使えるので上記変更は必要ありません。

余談ですが、簡易補完を使いたくなる長いメソッド名はたいてい複数の引数をとるので(そして、Smalltalk は引数をメソッド名中のコロンの後に挿入する変態文法であることを思い出してください)、引数の挿入場所まで一気に移動できる argument advance (Mac では cmd + shift + a、Win では alt + shift + a が使えないので代わりに ctrl + shift + a)を併用するとさらに便利です。

Mac版では他にも senders of it (cmd + n。Win版では alt + n なのでやはり問題なし) が使えなかったりしていろいろ不便ですが、query symbol などのキー操作でしか使えない機能と違いメニューに同等機能があるショートカットの場合、少々面倒でも右クリックメニューから実行できるのでこれ以上あまり深追いはしないことにします。^^;

▼タイマーの実験(簡単な並列処理)

タイマーの実装のキモは、カウントダウンをバックグラウンドで行なうマルチスレッド処理です。Smalltalk ではブロック(Ruby のブロック { } とは違い [ ] で括る)に fork というメッセージを送ることで別スレッドで処理を走らせることができます。なお、Smalltalk でスレッドは「プロセス」 と呼び、クラス名もそのまま Process なので他言語ユーザーは少々混乱するかもしれません。

残念ながら「チューチュー」と鳴らすことはできませんが、か細く「ピー」とビープを鳴らすことは Smalltalk beep という式でできるのでそのテストも兼ねて。

[ (Delay forMilliseconds: 3000) wait.
    Display flash: Display boundingBox.
    3 timesRepeat: [Smalltalk beep]
] fork.
3 + 4

この式をタイプするかコピペして改めて選択した後、右クリック → print it (p) すると、3 + 4 の結果の 7 を表示したあと 3秒ほど経ってから画面の反転とビープ音が鳴るはずです。

f:id:sumim:20190501145024p:plain f:id:sumim:20190501145038p:plain

▼まずはUIだけ部分的に作って試す

Smalltalk の古典的MVCフレームワークでは、StandardSystemView で外枠を作り、その中にプラガブルなビューを追加(add subview)してゆくことで UI を構築できます。モダンな GUI と違い、ウィジェットを自由な位置に配置するのではなく、ペイン(枠)として区画を割り当てるようにするのがポイントです。

Smalltalk環境でのコード片のお試しや動作テストは、通常はワークスペースでの do it や print it で試みるものなのですが、残念ながら Mini Squeak 2.2 ではシュリンクによる機能削減の煽りを受けてか、ワークスペースでのコード修正(テンポラリー変数の宣言のし忘れやタイプミスの指摘と自動修正)がうまく機能せず、普通の言語処理系のようにエラーだけ返してくるので Smalltalk環境のうまみがまったく失われてしまっています。

幸い、システムブラウザにはこれらのSmalltalk環境らしい機能がなんとか保たれているようなので、先に Timer クラスを作って、そのクラスメソッド open として試したいコードを記述しそれを更新してゆきながら試行錯誤を続けるのがよさそうだと判断しました。

まずシステムブラウザを起動し(デスクトップクリック → open... → browser)、左上のペインの右クリック → add item... でクラスカテゴリーを新しく追加します。ここでは MiniSqueak22-Timer としますが、気に入らなければ好きな名前に変えてもらって構いません。

f:id:sumim:20190430230957p:plain

クラスカテゴリーが追加されると、下のコードペインにクラス定義のためのテンプレートが現れるので次のように修正して Timer クラスを定義します。

Model subclass: #Timer
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MiniSqueak22-Timer'

修正が終わったら(あるいはここからコピペして置き換えるのでもOKです)、右クリックメニュー → accept して決定します。

f:id:sumim:20190430231434p:plain

上段2番目の枠に Timer が追加されれば成功です。すぐ下の ? を挟んで右側にある class ボタンをクリックしてクラスメソッド定義モード(クラスサイド)に切り替えてから、3番目の枠の no message をクリックすると下のコードペインに、こんどはメソッド定義のテンプレートが現れるので次のコードに置き換えてください。

open
    "Timer open"
    | timer topView |
    timer := Timer new.
    topView := StandardSystemView new model: timer.
    topView borderWidth: 2; label: 'Timer'.

    topView controller open

入力(もしくはコピペによる置き換え)が終わったら 右クリック → accept でコンパイルします(初回のコンパイル時にはイニシャルを尋ねられるので適当に答えてあげてください)。コンパイルが成功すると右上のペインに open が追加されます(Smalltalkではメソッド単位、つまりインクリメンタルなコンパイルが通常です)。コメントとして仕込んでおいた Timer open を選択して 右クリック → do it (d) すると空のウインドウを表示できます。

f:id:sumim:20190430233208p:plain

試しにいくつかサブビュー(ウィジェット)を追加してみましょう。上で定義した Timer class>>open メソッドを少々長くなりますが次のように変更して accept してください。

open
    "Timer open"
    | timer topView disp minBtn secBtn |
    timer := Timer new.
    topView := StandardSystemView new model: timer.
    topView borderWidth: 2; label: 'Timer'.

    disp := DisplayTextView new model: '00 : 00' asDisplayText; controller: NoController new.
    disp window: (0 @ 0 extent: 100 @ 40); centered.
    disp borderWidthLeft: 0 right: 0 top: 0 bottom: 2.
    topView addSubView: disp.

    minBtn := PluggableButtonView on: timer getState: nil action: #incMin.
    minBtn window: (0 @ 0 extent: 25 @ 25).
    minBtn borderWidthLeft: 0 right: 2 top: 0 bottom: 2; label: 'min'.
    topView addSubView: minBtn below: disp.

    secBtn := PluggableButtonView on: timer getState: nil action: #incSec.
    secBtn window: (0 @ 0 extent: 25 @ 25).
    secBtn borderWidthLeft: 0 right: 2 top: 0 bottom: 2; label: 'sec'.
    topView addSubView: secBtn toRightOf: minBtn.

    topView controller open

改めて Timer open を do it すると今度は内部にいくつかのウィジェットが割り振られたウインドウが開きます。

f:id:sumim:20190430234408p:plain

どうやらサブビューは window: で適当な相対的なサイズを指定し、addSubView:below:addSubVew:toRightOf: で追加しつつ配置してやるとうまいことやってくれるようです(←面倒くさがってまじめにコードを見ていない^^;)。ただデフォルトのウインドウのサイズがでかくて間延びしてしまうのでこれをなんとか対処してみましょう。

システムブラウザの instance ボタンをクリックしてインスタンスメソッド定義モードに戻し、no message をクリックして選択後、下のコードペインに次のメソッドを追加します。

initialExtent
    ^200 @ 200

ひとつのシステムブラウザでインスタンスサイド(instanceボタンをクリック)とクラスサイド(class ボタンをクリック)を行き来するのは面倒なので、もう一つシステムブラウザを開き(デスクトップクリック → open... → browser)こちらをクラスサイドブラウズ用に使うことにします。システムブラウザは何個でも開くことができるので、インスタンスサイド、クラスサイドの切り替えに限らず、必要ならどんどん開くとよいと思います。

Timer のクラスメソッドの open を呼び出し、Time open を選択して do it すると、今度は小さなウインドウで表示されます。

f:id:sumim:20190501002720p:plain

▼ min と sec ボタンが機能するようにモデルを整える

現時点では、タイマーの min ボタン(あるいは sec ボタン)を押すと、当然エラーになります(スタックトレースを表示したノーティファイアが表示される)。これは、モデルであるタイマー(Timer)にはサブビュー作成時に指定した incMinincSec などというメソッドはまだ定義されていないからです。

f:id:sumim:20190501003856p:plain

そこで次にモデルの Timer を UI のお試しができる程度に incMinincSec を含めて少し手を入れてみましょう。

まず、今の open の実装では disp サブビューのモデルは決め打ち('00 : 00' asDisplayText)なので、これをTimerに生成させ、その管理下に置く必要がありそうです。

モデルとしての Timer の実装をどうするかはいろいろ考えられそうですが、ここではシンプルに秒換算整数値の counter を持ち、incMin はそれに 60 を、incSec はそれに 1 を足すことにします(将来実装するカウントダウン機構ではこの counter を1秒経過するごとに減らしてゆく予定です)。また Timer は、秒換算値の counter を分と秒に分けて 00 : 00 形式で表示するDisplayText オブジェクトを生成・保持して disp サブビューのモデルとして提供し、適宜内容を更新して(それを伝播することで)表示を更新させます。

はじめに、TimercounterdispDispTextdisp サブビューのモデルのDisplayTextオブジェクトを保持する)の2つのインスタンス変数を追加します。インスタンスサイド用のシステムブラウザをクリックしてアクティブにした後、改めて instance ボタンをクリックする(あるいは、Timerをクリックしていったん選択を解除後、再びクリックして選択する)と Timer の定義式がコードペインに呼び出されるので、次のように変更して accept します。

Model subclass: #Timer
    instanceVariableNames: 'counter dispDispText '
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MiniSqueak22-Timer'

引き続き、インスタンスサイド(instance ボタンをクリックした方のブラウザ)に次のメソッド群を追加します。

…が、その前に今の Squeak Smalltalk では馴染みの、しかし Mini Squeak 2.2 には存在しない SequenceableCollection>>last: というメソッドを Timer>>dispStringFrom: を書くときにいつものクセでつい使ってしまっている^^;ので、これをあらかじめ追加しておきます。

新しいシステムブラウザを開き(デスクトップクリック → open... → browser)、上段右端のペインの右クリック → find class... → seq などとタイプし return/enter キー を押す

f:id:sumim:20190501152407p:plain

SequenceableCollection が選択されるので第3ペインの as yet unclassified をクリックして選択後、下のコードペインに次のコードを入力して accept します。

last: n
    ^ self copyFrom: self size - n + 1 to: self size

f:id:sumim:20190501153351p:plain

改めてTimerインスタンスサイド用のシステムブラウザに戻り、上段の3番目のペインの as yet unclassified を選択して下のコードペインにメソッド定義のテンプレートを呼び出してから、下のコードをコピペ→accept を繰り返せば次々にメソッドを追加してゆくことができます。

最初は、直前で追加した last: を使って、counter から 00 : 00 形式の文字列を生成する dispStringFrom: です。

dispStringFrom: int
    ^('0', (int // 60) asString last: 2), ' : ', ('0', (int \\ 60) asString last: 2)

f:id:sumim:20190501152715p:plain

以下、同様に続けてください。

dispString
    ^self dispStringFrom: counter
dispText
    ^self dispString asText allBold; yourself
initialize
    counter := 0.
    dispDispText := self dispText asDisplayText.
dispDispText
    ^dispDispText
counterLimit
    ^100 * 60 - 1
incMin
    counter := counter + 60 min: self counterLimit.
    self changed
incSec
    counter := counter + 1 min: self counterLimit.
    self changed
dispChanged
    dispDispText text: self dispText; changed
changed
    self dispChanged.
    super changed

以上、インスタンスメソッド群の定義が済んだところで、クラスサイド(class ボタンをクリックしてある方)のシステムブラウザに移動して、クラスメソッドとして改めて new を再定義します。この当時の Squeak Smalltalknew してインスタンスを生成する際に initialize を自動で呼ぶ仕様にはまだなっていなかったので、new を再定義してそれをやらせる必要があります。

new
    ^super new initialize; yourself

opendisp のモデルを決め打ちしているこの行

disp := DisplayTextView new model: '00 : 00' asDisplayText; controller: NoController new.

を次のように変更して accept します。

disp := DisplayTextView new model: timer dispDispText; controller: NoController new.

改めて Timer open してウインドウを開き、min や sec をクリックしてみましょう…。

ところがしかしここで問題発生。^^;

エラーこそ出ないものの、disp サブビューが更新される気配がありません。これはいったいどうしたことでしょうか?

dispサブビューが更新されない理由を探り、DisplayTextViewForTimer を作ってこれを使う

Timer ウインドウがアクティブな状態で、Mac では cmd + .(ピリオド)、Win では alt + .(ピリオド)をタイプすると、アクティブレッドに割り込みをかけられます。割り込み時点のスタックトレースが表示されたノーティファイアが開くので、そこで右クリック → debug するとデバッガーを起動できます。

f:id:sumim:20190501015157p:plain

デバッガーの最上段ペインのスタックトレースから StandardSystemView あるいは StandardSystemController がレシーバーのコンテキスト(スタックフレーム)を探してクリックして選択してから、最下段左側のペインの model をクリックして選択後、右クリック → inspect でインスペクターを開きます。

f:id:sumim:20190501015646p:plain

f:id:sumim:20190501015740p:plain

インスペクターの counter をクリックすると、(min や sec ボタンどちらかを1回でもクリックしていれば)確かに値は初期化時の 0 のままではないはずです。さらに念のため、dispDispText をクリックして選択してから 右クリック → inpect → 現れたインスペクターの text をクリックしてその値を確認してみても、disp サブビューのモデルである dispDispText はきちんと更新されている('00 : 00' のままではない)ことが確認できます。

f:id:sumim:20190501020742p:plain

では、どうして disp サブビューが更新されないのか気になるので、デバッガーでトレースして探ってみましょう。古典的な Smalltalkブレークポイントは、任意のメソッドのコード内の任意のレシーバーに対し halt というメッセージを送る記述を挿入することで設置できます。ここでは dispChanged メソッド内で dispDispText のテキストを更新した後にモデルの変更を通知する changed を送る直前に設置してみます(次図の反転部分)。

f:id:sumim:20190501021849p:plain

accept してコンパイルしてから、Timer ウインドウの min もしくは sec ボタンをクリックするとノーティファイアが現れるので、右クリック → debug でデバッガーを起動します。

f:id:sumim:20190501022123p:plain

デバッガーの最上段のペイン(スタックトレース)で 右クリック → step (t) するか、同ペイン内にマウスカーソルを置いた状態でTキーをタイプすると、デバッガー中央のペインにブレークポイントを設置したメソッドのソースと次にコールされるメソッドが(Smalltalk風に言うと「次に送られるメッセージが」)反転表示された状態で呼び出せます。

f:id:sumim:20190501022914p:plain

うっかりこのまま連続して step (t) してしまうと折角止めたコンテキスト(スタックフレーム)を抜けてしまうので、次に、最上段のペインで 右クリック → send (e) して changed がコールされた直後のコンテクストにステップインします。

f:id:sumim:20190501023101p:plain

この調子で、changed: self を send (e)、dependents を step (t) 、do: [:aDependent | aDependent update: aParameter を send (e)、sizeat: index は step (t)、value: (self at: index) を send (e) 、update: aParameter を send (e) と順に処理を辿っていくと、果たしてついに DisplayTextView が通知を受け取って何をしているかが明らかになります。

f:id:sumim:20190501024157p:plain

なんと、self を返している、つまり、何もしていないのです。w これでは画面は更新されないはずです。おそらく、DisplayTextView のユースケースではモデルが更新されることが想定されていないのでしょう。

しかたがないので、今回のタイマー用に、DisplayTextView を継承した DisplayTextViewForTimer なるクラスを作ってちゃんと画面更新処理をする update: を再定義して、これを使うことにします。^^;

DisplayTextView subclass: #DisplayTextViewForTimer
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MiniSqueak22-Timer'
update: param
    self model: model; displayView.
    super update: param

f:id:sumim:20190501024952p:plain

もちろん Timer class>>open メソッド内の DisplayTextView を使用した行

disp := DisplayTextView new model: timer dispDispText; controller: NoController new.

の該当部分を DisplayTextViewForTimer に置き換えて

disp := DisplayTextViewForTimer new model: timer dispDispText; controller: NoController new.

accept してから、Timer open を do it して動作を確認します。

ブレークポイントを設置したままなので、相変わらずノーティファイアが表示されますが、右クリック → debug でデバッガーを起動して step (t) してブレークポイントを設置したメソッド(のコンテキスト)を呼び出した状態で最上段ペインの右クリック → versions でメソッドの変更履歴を呼び出し、

f:id:sumim:20190501042833p:plain

ひとつ前のバージョンをクリックして選択して 右クリック → fileIn selections で元に戻せます。

f:id:sumim:20190501042856p:plain

デバッガーは同じく最上段ペインの右クリック → proceed で閉じてしまってください。

改めて、min もしくは sec ボタンをクリックすると、めでたく disp サブビューが更新されるはずです。

f:id:sumim:20190501031217p:plain

▼終わりに

実装の途中ですが、思いのほか長くなってしまったので、ここらへんで終わりにします。

古いうえ制約の多い Mini Squeak 2.2 を使ってではありましたが、Smalltalk環境でのプログラミングのおもしろさの一端を垣間見ていただけたかと思います。今でこそ当たり前ですが、簡易とは言えメソッド名の補完やコンパイル時のスペル修正機能、メソッド単位のインクリメンタルなコンパイルやバージョン管理機能は遥か昔の1980年代にすでに存在した技術であったことを驚きをもって実感してもらえたなら幸いです。

試行錯誤を経て完成させた版のタイマーのコードは下のリンク先に置いておきますので、Mini Squeak 2.2 環境に fileIn した後、システムブラウザで読んだり、実際に動かしてみたり、さらに手を加えてしていろいろ試して遊んでみてください。

[追記: 2019-05-02] 仕様変更(スタートしたら start ボタンは stop ボタンに & カウントダウン中の min、sec ボタンで押下で延長を可能に)

@miwa719 さんに開発の経緯のツイートまとめを教えてもらいました!

start ボタンはカウントダウンをスタートすると stop ボタンに変わることが分かったので、そのように変更してみました。MiniSqueak22-Timer.st に続いて Timer-toggleStateTweak.cs というパッチを用意しましたのでこれを fileIn するとコードが変更されます。なお念のため .cs は C# …ではなく^^;「change set(変更の集合)」の略で、Smalltalk において古くから用いられている拡張子です。

f:id:sumim:20190502004944p:plain

さらに悪ノリして、カウントダウン中でも min や sec ボタンを押せるようにして、押した分だけ時間を延長することができるようにもしてみました。複数スレッドからの counter の変更には排他制御を使っています。Timer-toggleStateTweak.cs の後にさらに fileIn すると仕様が更新されます。

「使わないと損をする Model-View-Controller」(Smalltalkの古典的MVCの解説)のサンプルコードを SqueakJS のデモ画面(Mini Squeak 2.2)で動かす

「使わないと損をする Model-View-Controller」 はオブジェクト指向Smalltalk関連の著作の多い青木淳氏による(今となっては)古典的な SmalltalkMVC の解説記事で、ごく初期の“コントローラーが頑張るMVC”、そこから少し進化した“依存性を利用したMVC”、更に拡張性を意識した“プラガブルを利用するMVC”の三つに焦点を当て、シンプルなコードを示しながらそれぞれのポイントが簡潔に語られていて参考になります。

MVC のルーツについての理解を深める目的であれば、せいぜい書面でコードを追うだけでもよさそうなものですが、せっかくならやはり実際に動かしてみたい…と思うものですよね(え?思わない?^^;)。とはいえ(ともあれ)、当該文書掲載の Smalltalk のコードをそのまま写経できそうな商用の処理系の入手はほぼ不可能なので、オープンソースSqueak Smalltalk のごく初期の実装(バージョン 2.2。1998年頃リリース)向けに少しだけいじって動かしてみました。(なお参考まで、同様の試みは Smalltalk勉強会 第30回 でもなされています。このときは Squeak1.31 が用いられました。)

squeak.org

Squeak の特に初期のバージョンは、1980年代の Smalltalk-80 の製品化直前のバージョンをベースに Lisa/Macintosh(旧Mac)向けに作られた Apple仮想マシンVM)で動くゼロックス純正 Smalltalk-80 である「Apple Smalltalk」の“生まれ変わり”とも呼ぶべき処理系で、その後も Scratch の当初バージョン(~1.4)や、その先輩格にあたる Squeak Etoys の開発に(こちらは今でも)使われていたり、現在活発に開発が進められている Pharo Smalltalk のフォーク元でもあります。ゼロックス系列外の商用やファンお手製のフルスクラッチ処理系とは違い、ゼロックスパロアルト研究所で培われた仮想イメージ(と付随するソースコード)をもとに、なおかつ Pharo などほどにアグレッシブな改変は行なわれていないこともあって、オリジナル Smalltalk-80 の特徴を(特にソースコードレベルで)色濃く残しているため、今回のように歴史を振り返る際にたいへん重宝します。

Squeak2.2 は公式サイトに アーカイブされている ので、これを入手してインストールするのでもよいのですが、実際に動かすとなると動作が不安定であったり何かと面倒なことがあるのでここでは SqueakJS のデモ画面で動いている Mini Squeak 2.2 を使うことにしました。SqueakJS はその名の通り JavaScriptSqueak SmalltalkVM を実装することで、Webブラウザで手軽に Squeak環境を動かしてみたいときに使える便利な処理系です。また、Mini Squeak はメモリの少ないモバイル機器などでも動かせるように最低限の機能にシュリンクしたもので、かなり重要な情報や機能がごっそり削られてしまっているものの、非常にお手軽に SqueakJS サイトでワンクリックで起動できること、MVC に限っては当該サンプルコード程度なら問題なく動かせるので選びました。なお、SqueakJS それ自体の能力としては、特にシュリンクが必須ということはなく、比較的最近のバージョンの Squeak でもフルイメージのまま動かせます。念のため。

squeak.js.org

Squeak も大筋で踏襲する70年代成立の Smalltalk環境の操作スタイルは、旧Mac や Win(2.0 以降)、macOS の前身である NeXTSTEP での GUI 操作スタイルの手本にもなっていますし、Squeak は特に旧Mac から逆輸入したウィジェット(たとえばウインドウのタイトルバーやクローズボックスなどの追加)もあり、さほどの違和感なく(旧Mac経験者ならなおさら)操作できるかと思います。

ん?なにを当たり前なことを…と思われる向きは、1970~80年代初頭の“GUIカンブリア爆発”同時期に同じゼロックスで開発された他の GUI である Star/ViewPoint、Interlisp-D/Medley などをぜひ一度どこかで触ってみてください(ViewPointやInterlisp-Dは Starのエミュレーター で体験できます)。予備知識やマニュアル無しで操作するのはほぼ不可能ですので…w 70年代に成立したあるGUIが今もほぼ違和感なく使えるというのは実に希有なことなのです! この点に限ってはSmalltalkをパクってくれたジョブズに感謝しないとですね^^;。

閑話休題

それでも Squeak2.2 のように特に古いバージョンでは、スクロールバーなど独特な部分もあるので、ちょっとだけ注意点を列挙しておきます。

  • 基本的に右クリックによるコンテキストメニューを使う。(後の Apple の発明であるメニューバーやプルダウンメニューはありません)
  • キーボードからの文字入力は原則として、アクティブ(最前面。これは同じ)、かつ、マウスポインタで指示(フォーカス)された区画(ペイン)に対して行なわれる。
  • Macでは、キーボードショートカットのキーコンビネーションは alt + ~ が基本(ctrl + ~ も alt + shift + ~ の代替として使用)。
  • 画面の再描画はかなりルーズ。(汚れてきたら、デスクトップクリック → restore display で適宜再描画)
  • スクロールバーは左側にフリップアウト(フォーカスのインジケーターも兼ねる)。スクロールボックスドラッグもしくは移動先のクリックで移動、左傍クリックで上スクロール(矢印は逆。上端からの距離で速度調節)、右傍クリックで下スクロール(同)。Squeakではさらにペイン寄りの場所でメニューポップアップも可能(ワンボタンマウス対策)。

f:id:sumim:20190409164441p:plainf:id:sumim:20190409164501p:plainf:id:sumim:20190409164523p:plainf:id:sumim:20190409164533p:plain

ちなみに、Smalltalkでは第一マウスボタンを赤ボタン(red button)、第二マウスボタンを黄ボタン(yellow button)、第三マウスボタンを青ボタン(blue button)と呼び、コードでもそう表現します。中ボタンはともかく、現在のように「左ボタン」「右ボタン」などと表現しなかったのは、XEROX Alto時代、マウスのボタンの並びを横並びに限定することを避ける(実際にボタンを縦並びにしたマウスも試された)ためです。

f:id:sumim:20190409164549p:plain http://toastytech.com/guis/salto.html より

この慣習は現在のPCのマウスにおいて、使う黄ボタンと青ボタンを入れ替えて、黄ボタンクリックを右ボタンクリックに(代わりに、あまり使わない青ボタンはやや使いにくいスクロールホイールボタンに)割り当ててもコードや表現に読み替えの必要がないことにも役立っています。

▼Mini Squeak 2.2 向けサンプルコードと、それが使用する Mickey.form ファイルの入手

.zip にまとめて次のリンク先に置きました。ダウンロードして展開すると、MVC-Study.mini.st と Mickey.form の2つ(後述“おまけ”の Form-simpleDraw.mini.cs を含めれば3つ)のファイルが得られます。MVC-Study.mini.st はプレーンテキストファイルですが、展開時、展開用ツールが改行文字を自動で置き換えてしまわないよう注意してください。

▼SqueakJS のデモ(Mini Squeak 2.2)の起動と2つのファイルのインポート

WebブラウザChrome を推奨します。

Mini Squeak 2.2 が起動すると、ブラウザ内に Squeak 環境が表示されます。たとえば、「Welcome to Mini Squeak 2.2」ウインドウ内の 100 factorial をドラッグして選択してから 右クリック → print it (p) すると 100 の階乗が計算され結果が直後に挿入されます。

f:id:sumim:20190409164617p:plain

f:id:sumim:20190409164632p:plain

IDEのターミナルペイン内も含め、コマンドラインインターフェースでの言語処理系利用に慣れていると少々戸惑われるかもしれませんが、これが Smalltalk における典型的なコード実行スタイルです。カット&ペースト(ポップアップメニューを使ったものは Smalltalk が最初)と同様のワープロ内操作感覚で、選択した式などのコード片を気軽に評価(do it、print it)できるのが特徴です。

先ほど展開した MVC-Study.mini.st と Mickey.form をデスクトップ(Webページ)にドロップインします。何もフィードバックはありませんが、デスクトップクリック → open ... → file list で開く、簡易ファイラ兼エディタの右上のファイル一覧ペイン内にインポートされたファイルを確認できます。

f:id:sumim:20190409164649p:plain

ちなみにこれらのファイルはブラウザのローカルストレージに保存されてます。(不要になったら各ブラウザのツール、たとえば Chrome なら その他のツール → デベロッパー ツール → Application → Clear storage → Clear site data で削除できます。Squeak環境内で変更を加えたり、新しく作ったファイルは適宜 https://squeak.js.org/run/ ページの太枠点線内の一覧からダウンロードできます。)

MVC-Study.st のファイルイン(コードの読み込み)

そのまま file list のファイル一覧から MVC-Study.mini.st をクリックして選択し、右クリックメニューから file in します。

f:id:sumim:20190409164710p:plain

デスクトップクリック → open... → browser でシステムブラウザ(クラスブラウザとも呼ぶ)を開き、上段左端のペイン(枠)を最後までスクロール(マウスボタンをペイン内に入れてフォーカスし、左側にフリップアウトしたスクロールバーのスクロールボックスを下までドラッグ)して、MVC-Study が追加されていれば file in(コードの読み込みと一括コンパイル)は成功です。

f:id:sumim:20190409165144p:plain

▼Mickey1 class>>example(Mickey1 のサンプルコード)の実行

そのまま、上段第二ペインより Mickey1 → その下の class ボタンをクリック(クラスメソッドへの切り替え) → 第三ペインから examples → 第四ペインより example を次々にクリックして選択して下段のコードペインに Mickey1 class>>example メソッドのコードを呼び出します。

コード内のコメント "Mickey1 example" のダブルクオーテーションで括られた中身(Mickey1 example)だけを選択し(ドラッグしてもよいですが、ダブルクオーテーションの内側、つまり、最初のダブルクオーテーションのすぐ右側か、行末のダブルクオーテーションのすぐ左側をダブルクリックすると簡単です)、右クリック → do it (d) します。

f:id:sumim:20190409165211p:plain

すると、Mickey1 ウインドウが表示されます。ウインドウ内で右クリックすると magnify、shrink メニューがポップアップするのでいずれかを選択するとコードされたとおり、ネズミ(?)の絵が二倍に拡大(magnify)か、半分に縮小(shrink)されて表示されます。

f:id:sumim:20190409165227p:plain

f:id:sumim:20190409165242p:plain

▼変更個所と追加したサンプルコード(Mickey2 class>>example2)について

  • Mini Squeak 2.2 は Form>>shrink:by: を削除してしまっていて使えないので、簡単のため 0.5倍の拡大として実装。
  • 同じく DisplayObject>>displayOn:at:clippingBox: も削除されていて使えないので、displayOn:at:clippingBox:rule:fillColor: に置き換え。
  • Mickey3 class>>example の model: による改めてのモデル設定は不要(on:aspect:menu: で設定済み)なので削除。
  • Squeak には ActionMenu がないので SelectionMenu で代替。
  • Mikcey3Controller のクラス変数(MickeyYellowButtonMenuとMickeyYellowButtonMessages)は不要なので削除。
  • Mickey2 クラスメソッドに2ペインの example2 を追加。
Mickey2 class >> example2
    "Mickey2 example2" "MODEL magnify"
    | topView subViewLeft subViewRight |
    Smalltalk at: #MODEL put: Mickey2 new.
    topView ← StandardSystemView new.
    topView label: 'Mickey2'.
    topView borderWidth: 1.
    subViewLeft ← Mickey2View new.
    subViewLeft model: MODEL.
    subViewLeft borderWidth: 1.
    subViewLeft insideColor: Color white.
    subViewRight ← Mickey2View new.
    subViewRight model: MODEL.
    subViewRight borderWidth: 1.
    subViewRight insideColor: Color white.
    topView addSubView: subViewLeft.
    topView addSubView: subViewRight toRightOf: subViewLeft.
    topView controller open

どちらのペインを操作しても、他方が同時に更新されるのを確認してください。

f:id:sumim:20190409165328p:plain

f:id:sumim:20190409165339p:plain

モデルの変更がそれを参照しているビュー(複数可)に自動的に伝播するのは、依存性を利用したMVC(ここでの例ではMickey2とMickey3)のキモですね。

ここで「複数可」と書き添えましたが、この振る舞いはビュー・コントローラーを介さずに、直接モデルを操作した場合でも同様です。たとえば、二つ目のコメントの MODEL magnify を do it (d) すると Mickey2 ウインドウ内での操作を介さずにビューを更新できます(Mickey2 ウインドウが他のウインドウの裏にある場合、前面のウインドウの内容も重ね書きされてしまうのはご愛敬。デスクトップクリック → restore display で直せます)。

f:id:sumim:20190409165407p:plain

f:id:sumim:20190409165428p:plain

あくまでモデルは(コードの上では)ビューのことを知らない、というのがミソですね。

▼回転機能(rotate)を追加してみる

同文書にあるように Mickey3 に回転機能(rotate)を追加してみましょう。

まず、画像を回転するのに使えそうなメソッドがあるか探します。適当な場所(デスクトップクリック → open... → workspace でワークスペースを新たに開くなどして)で rotate などとタイプして入力してから選択後、右クリック → more... → selector containing it (W) を選ぶと、メソッド名(セレクター)に「rotate」を含むメソッドの一覧が表示されます。

f:id:sumim:20190409165455p:plain

f:id:sumim:20190409165509p:plain

Form>>rotateBy:centerAt: が使えそうなので、これで rotate 機能を実装します。まず、システムブラウザの class がハイライトしていたら instance ボタンをクリックしてインスタンスメソッド閲覧モードに戻してから、Mickey3 → manipulation をクリックして選択し、下のコードペインに次のコード(Mikcey3 >>以降)をタイプして入力します。

Mikcey3 >> rotate
    form _ form rotateBy: #right centerAt: 0@0.
    self changed: #rotate

入力を終えたら 右クリックメニュー → accept (s) してコンパイルします。

f:id:sumim:20190409165530p:plain

なお初回のコンパイル時には、バージョン管理の際に記載するイニシャルを尋ねられるので何か適当に入力して enter してください。

f:id:sumim:20190409165622p:plain

コンパイルが通ると、上段右端のメソッド名一覧ペインのリストが更新され、rotate が追加されます。

続けて、上段第三ペインの accessing → 第四ペインの menu を選択し、下のコードペインのメニュー項目(selections: の引数の配列)に rotate を追加します。入力を終えたら accept (s) でコンパイルも忘れずに。

f:id:sumim:20190409165646p:plain

これで rotate の追加は終わりです。Mickey3 flushMenus; example を do it (d) するとメニューに rotate が追加され機能するはずです(flushMenus; はメニュー内容を更新後、初回のみ必要です)。

f:id:sumim:20190409165701p:plain

f:id:sumim:20190409165814p:plain

▼おまけ

Form-simpleDraw.mini.cs は、Mickey.form に自分で描いた絵を使いたい方向けのコードです。file list などを使ってファイルインして Form simpleDraw を do it すると、カーソルが「+」に変わるので適当な余白に絵を描いてください。シフトキーを押すと描画を終了するので、Mickey.form の上書きするの確認(overwrite the file を選ぶ)に答えた後、引き続き左上から右下に向けてドラッグして矩形範囲を選択すると Mikcey.form の内容を選択して指定した矩型範囲(に描かれた絵)で置き換えることができます。

f:id:sumim:20190409165833p:plain

Mini Squeak 2.2 では、画像(Form)を保存するためのメソッドがごっそりそぎ落とされていたためいろいろなメソッドを復旧する必要がありましたが、simpleDraw のコード自体はたったこれだけです。

Form class >> simpleDraw
    "Form simpleDraw"
    | pen file |
    pen ← Pen new.
    Cursor crossHair show.
    [Sensor shiftPressed] whileFalse: [
        Sensor redButtonPressed
            ifTrue: [pen goto: Sensor cursorPoint]
            ifFalse: [pen place: Sensor cursorPoint]
    ].
    Cursor normal show.
    file ← FileStream newFileNamed: 'Mickey.form'.
    file binary.
    Form fromUser writeOn: file.
    file close

DCI パラダイムの提唱者、Reenskaug 氏の BabyIDE で遊ぶ - Squeak4.5対応版向け

久しぶりに DCI がらみで BabyIDE にヒントがあるかもしれないな…と思われる話題に接したのを機に、以前遊んでみた BabyIDE のその後を調べてみたところ、引き続き Squeak3.10.2 上で開発が続けられている BabyIDE-2018.07.31.ZIP というのを発見して驚きました(氏は御年88歳!)。同時に、それよりちょっとバージョンは古そうですが、プラットフォームとしては比較的最近の Squeak4.5 にインストールして使えそうな SqueakMap パッケージ BabyIdeAllInOne の存在を知ることもできたので、今回はこれで遊んでみることにしました。

 

DCI と BabyIDE については過去の記事を参考にしてください。

BabyIDE もかなり癖のある処理系・IDE ですが、それを実装している Squeak Smalltalk という処理系・IDE も通常の言語からするとずいぶん風変わりなものなので、目を慣らすために次のチュートリアルをざっと眺めたり、できれば実際に Squeak を(チュートリアルに合わせて http://ftp.squeak.org/3.9/ から必要なファイルを得て 3.9 を)インストールして、実際に体験してみるのも一興かと思います。

 

Smalltalk 処理系は Squeak 以外にもいくつかありますので、こちらも参考まで。

qiita.com

 

Squeak4.5の入手と起動

Squeak Smalltalk公式ページから今ダウンロードできるのは最新版の 5.2 ですが、BabyIdeAllInOne パッケージが対象にするそれより少し前の 4.5 は https://ftp.squeak.org/ から Squeak-4.5-All-in-One.zip として入手可能です。

これをダウンロードしてから適当な場所に展開し、Windowssqueak.bat、Mac なら Squeak-4.5-All-in-One.app のダブルクリックで、Linuxsqueak.sh で起動できるはずです。ただ少し古いバージョンなのでOSの更新状況によってはうまく動かないかもしれません。適宜対処してください。^^;

ともあれ、ダウンロードして展開するだけ(古いバージョンだと、最低限必要なファイルを知って揃える必要はありますが…)という手軽さと環境を汚すことなくインストール(不要になれば削除でアンインストール)できる点は Squeak Smalltalk の非常に良いところなので、風変わりだと食わず嫌いをせず、ぜひ気軽に試してみていただきたいです。w

 

BabyIdeAllInOne-1.4 のインストール

Squea4.5 の起動後、Apps → SqueakMap Categories (もしくは、デスクトップクリックでポップアップするメニュー → open... → SqueakMap Categories)で Categorical SqueakMap Package Loader を起動し、まず Update します。

f:id:sumim:20190222154739p:plain

Categorical SqueakMap Package Loader

パッケージリストがうまく更新されると左側のペインにカテゴリーリストが表示されるので、Development tools を選択し、続いて右のペインから BabyIDE (->1-2.9) ではなくBabyIdeAllInOne(1.4) を選択し Install します。

f:id:sumim:20190222160837p:plain

BabyIdeAllInOne

Transcriptウインドウが開いてコメントや警告がいろいろと出ますが、サブパッケージのロードの失敗( ERROR: Could not download version BB8aMoveShape-TRee.3 from http://www.squeaksource.com/DCI )のような致命的なものでなければ大丈夫です。

f:id:sumim:20190222161909p:plain

インストールが正常に終わったときの画面の様子

Transcriptで警告をスルーしつつスクロールして遡ると

 

========== Planning tool BB9Planning-TRee.5 ==========

Prokon: An activity network planning program
See BB9DBase comment.

 

のようにサンプルプロジェクトの簡単なコメントを読むことができます。この手の情報はソースから得られるので気にしなくてよいかと思いますが、Transcript は以降、結果出力表示( Transcript cr show: 'hoge' )等にも利用するのでそのままにしておきます。Categorical SqueakMap Package Loader の方はもう不要なので閉じてしまいます。

 

次回起動時以降、この状態から始められるようにネズミ(Squeakアイコン)メニューから Save Image を選んで環境を保存しておきましょう。

この手のネット経由のインストールは時間が経つとうまく動作しなくなるのが常なので、動くうちにとりあえずインストールだけでも済ませておいて、この環境まるごと保存機能で“動態保存”しておくのがよさそうです。

 

f:id:sumim:20190222165600p:plain

インストールが完了した環境の保存

 

組込みサンプル(DCIアプリケーション)の定義ブラウズと実行

組込みのサンプル(DCIアプリケーション)は Apps メニュー → BB1: IDE → Select Application メニューにリストされた BB2 以降を選ぶと babyIDEウインドウを開いて定義を閲覧・編集することができます。

 

コンテクスト(Context)とデータモデル(Data)のブラウザは通常のSmalltalkのクラスブラウザに似た感じで使えます。

f:id:sumim:20190222165925p:plain

コンテクストブラウザ

 

コンテクストブラウザの SWAP TO INTERACTION ボタンでロールブラウザに切り替えられます。

左上にコンテクストがリストされるのでどれかを選ぶと、右側のペインにロールがダイアグラムで表示されます。メソッド付きのロール(太枠で表示)をクリックして選択すると、左下のペインに定義されたメソッドがリストされるので、クリックすると右側ペインに定義が表示されます。ロールの上でマウスポインタをホバーさせると、バインディング吹き出しで表示されます。

f:id:sumim:20190222165155p:plain

ロールのバインディングとメソッド

DCIアプリケーションの起動時は、同じく Apps メニューから(前述の BB1: IDE のサブメニューを経由せず、直接)BB2 以降のアプリケーション名を選択します。ちなみに、BB2Shapes は右クリックでアニメーション停止後、再度右クリック→ EXIT で終了できます。

 

f:id:sumim:20190222170444p:plain

BB2Shapesアプリケーションとその終了

各アプリケーションの意義やそれを通じて示したいことについては DCI Documentation - BabyIDE examples などのドキュメントに書かれています。かならずしもGUIのあるアプリケーションばかりではないですし、GUIがあってもあまり直感的な実装にはなっていないので、試しに走らせる前にどんなアプリケーションなのかは知っておく方がよいでしょう。

 

ロールがうまく定義されないバグの修正

コンテクストに新しいロールを定義したときに、内部的に対応するクラスが自動生成されるカラクリがうまく動かないようだったので、いろいろ定義して遊んでみる前にこれを修正しておきます。

まず、Tools メニューから Workspace を選んでワークスペース(コードを実行できる書き捨てのメモ帳)を開きましょう。ウインドウ内にマウスポインタを移動してから「ensurerole」などと入力し、alt + q (Mac では cmd + q。以下同様)とタイプしすると「ensureRoleClassFromRoleName: notifying:」とメソッド名が補完されるので(うまくできないときはここからコピペするかタイプして入力してください)、続けて alt + m とタイプする(もしくは右クリック → more... → implementors of it を選択する)とこのメソッドの定義が表示されます。 

「, '_'」の部分を削除して alt + s(もしくは、右クリック → accept )でコンパイルすれば作業は終了です。初回のコンパイル時にイニシャルを尋ねられると思うので、適当に答えてやってください。

なお、ここで入力したイニシャルやニックネームは versions ボタンを押して表示することができるメソッドの変更履歴の管理などに使われます(たとえば、変更前の Trygve Reenskaug 氏記述のバージョンをクリックして選択後、revert ボタン(あるいは、右クリック→ revert to selected version )でいつでもこの変更を取り消すことができます)。

[変更] 修正方法を変えました。すでに , '_' の削除で対処済みの場合は、早速ですが^^; ensureRoleClassFromRoleName: notifying: メソッドの定義を先述の方法で呼び出し、versions → 修正前の Trygve 版選択 → revert で元に戻してから以下を続行してください。

ウインドウ内にマウスポインタを移動してから「createrole」などと入力し、alt + q (Mac では cmd + q。以下同様)とタイプしすると「createRoleClass: roleName: notifying:」とメソッド名が補完されるので(うまくできないときはここからコピペするかタイプして入力してください)、続けて alt + m とタイプする(もしくは右クリック → more... → implementors of it を選択する)とこのメソッドの定義が表示されます。 

f:id:sumim:20190224002248p:plain

createRoleClass:roleName:notifying: メソッドの定義

「roleClassName」に続けて「 asString storeString」を追加して alt + s(もしくは、右クリック → accept )でコンパイルすれば作業は終了です。初回のコンパイル時にイニシャルを尋ねられると思うので、適当に答えて Accept してください。

f:id:sumim:20190224003609p:plain

初回コンパイル時に要求される開発者のイニシャル

 

なお、ここで入力したイニシャル(ニックネームやファーストネームでも何でもよい)は versions ボタンを押して表示することができるメソッドの変更履歴の管理などに使われます。たとえば、変更前の Trygve Reenskaug 氏記述のバージョンをクリックして選択後、revert ボタン(あるいは、右クリック→ revert to selected version )でいつでもこの変更を取り消すことができます。

f:id:sumim:20190224002633p:plain

記録されているメソッド定義の変更履歴

ついでに、最後に使うソースコード閲覧用の HTML ファイル生成時にロールメソッドを列挙できない同根のバグも退治しておきましょう。同じようにワークスペースなどで「rutimerole」などとタイプした後、alt + q で「runtimeRoleClassForRoleName:」と補完してから alt + m でメソッド定義を呼び出します。「roleName」の前に「'_' , 」を挿入し alt + s(もしくは、右クリック → accept )でコンパイルすれば終わりです。

f:id:sumim:20190224014155p:plain

ソースの HTML 出力時にロールメソッドの列挙ができないバグを修正

無事コンパイルできたら、インストール直後と同じく次回以降の起動でも有効になるよう忘れずに、Squeakメニュー → Save Image しておきましょう。

 

ローンシンジケートの例を組む

こちらのローンシンジケートの例

を端折ったものを書いてみます。内容としてはエヴァンスのDDD本の8章と10章などに登場する投資銀行シンジケートローンを管理する巨大アプリケーションのコアを開発したときの話を基にしたもので、複数の投資会社(銀行)が共同で出資して金を貸し出したり、その一部が返済された場合に、各社の負担額はどう変わるか?といった処理を記述する課題です。

 

DCIアプリケーション「BBLoanSyndicate」の作成

Apps → BB1: IDE → other... → BBLoanSyndicate と入力 → Accept すると空の babyIDE が開きます。

f:id:sumim:20190223013915p:plain

何も表示されていない babyIDE

データモデルの定義

まず、ウインドウタイトルバー右側にある青い▼ボタンをクリック して new projection... を選択します。すると、Error: BUGGY OPERATION というノーティファイアが現れますが、ひるまず Proceed すると入力を促されるのでそこで Data とタイプしてから Accept します。再び、Error: buggy method. と注意されますが、無視して Proceed すると Data タブとクラスブラウザライクな画面が現れます。

f:id:sumim:20190223014624p:plain

Data タブが追加された babyIDE

下のペインの内容を以下のように書き換えるか、ここからコピペしてコンパイル(alt + s もしくは右クリック → accept )します。

 

Object subclass: #BBFacility
	instanceVariableNames: 'limit loan shares'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'BBLoanSyndicate-Data'

f:id:sumim:20190223015348p:plain

追加された BBFacility クラス

同様に BBFacility の定義を次のように書き換えて、BBLoan を定義します。

Object subclass: #BBLoan
	instanceVariableNames: 'shares'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'BBLoanSyndicate-Data'

f:id:sumim:20190223015806p:plain

追加された BBLoan クラス

続いて、それぞれのクラスに暗黙に呼ばれる初期化メソッド(initialize)を定義します。

  • クラス名リスト枠(上段右端)で BBFacility クラスをクリックして選択
  • メソッドカテゴリーリスト枠(上段右から三番目のペイン)で no messages をクリック
  • 次のメソッドを入力し、alt + s でコンパイル
  • メソッドカテゴリーリスト枠が no messages から as yet unclassified に変わるので、右クリック → rename category... → initialization と入力 → Accept で変更
initialize
	shares := Dictionary new

f:id:sumim:20190223021110p:plain

初期化メソッドの定義

同様の操作で BBLoan にも同じ内容の initializa メソッドを定義します。

f:id:sumim:20190223021520p:plain

BBLoan>>initialize も同様の操作で定義

後ほどインスタンス変数へのアクセッサーメソッドを自動生成する作業がありますが、とりあえずデータモデルの定義はこれで終わりです。(簡単のため元コードの Share と SharePie は定義しないことにします。)

 

コンテクストの’定義

データモデルのときと同様にコンテクストを定義するためのContextタブを追加します。

ウインドウタイトルバー右側の青い▼ボタンをクリック して new projection... を選択し、ノーティファイアが現れてもひるまず Proceed しつつ Context と入力 → Accept します。

現れたContextタブ(ボタン)をクリックして選択したあと、データモデルのときと同様に下のペインにコンテクストBBLoanSyndicateCtxを次のように定義します。

BB1Context subclass: #BBLoanSyndicateCtx
	instanceVariableNames: 'facility'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'BBLoanSyndicate-Context'

f:id:sumim:20190223024039p:plain

Context タブの追加と BBLoanSyndicateCtx クラスの定義

データモデルおよびコンテクストへのアクセッサーの一括定義

以下ではデータモデルおよびコンテクストのインスタンス変数に対するアクセッサーメソッドが定義済みであることを前提にしているので、このタイミングでこれらを自動生成しておきます。

適宜、タブを切り替えて、Dataタブの BBFacility、BBLoan、Contextタブの BBLoanSyndicate をクリックして選択してそのままマウスポインタを枠から出ないようにして(枠の中で)右クリック → create inst var accessors を選択します。

f:id:sumim:20190223024810p:plain

アクセッサーメソッドの自動生成

アクセッサーメソッドを自動生成したクラスのメソッドカテゴリーリスト枠には accessing プロトコルが追加され、そこにインスタンス変数と同名のゲッターと、それにコロンを付したセッターが定義されていることを確認します。

f:id:sumim:20190223025142p:plain

自動生成されたアクセッサーメソッド群

 

ロールの定義

さて、次にロールの定義なのですが、Contextタブに先述の SWAP TO INTERACTION ボタンが見当たりません。

f:id:sumim:20190223025806p:plain

Context タブに SWAP TO INTERACTION ボタンがない

これは、タイトルバーの青▼ボタン → set application... → BBLoanSyndicate を選択することで babyIDE をリフレッシュすればよいようです。Data、Contextタブの表記も簡潔になり、改めて Contextタブをクリックして切り替えることで SWAP TO INTERACTION ボタンもちゃんと現れるようになります。

f:id:sumim:20190223030448p:plain

リフレッシュ後の babyIDE ウインドウ

では改めて、SWAP TO INTERACTION ボタンをクリックしてロールの定義を始めましょう。

f:id:sumim:20190223030756p:plain

ロール定義画面

コンテクストに参加するロールとそのインタラクション(メソッド)を定義します。

まずロールとそれら相互の依存関係を定義します。ここまでの Smalltalk の伝統的なクラスブラウザの作法でのクラス定義と違い、楕円で示されたロールを作り、依存関係はそれらを矢印でつなぐことでダイアグラムとして図的に記述します。

  • 左上枠のBBLoanSyndicateCtx(コンテクスト)をクリックして選択
  • 右上の枠で右クリックして add role → Lender と入力 → Accept (alt + s)
  • Lender ロールを示す楕円を枠内の適当な場所をクリックして配置
  • 同様に、AmountPie、PercentagePie を作成して配置
  • Lender を右クリックして add link → AmountPie をクリック
  • AmountPie を右クリックして add link → PercentagePie をクリック

f:id:sumim:20190223031639p:plain

定義されたロールと依存関係

インタラクション(ロールメソッド)の定義

そして DCI の I 、各ロールにインタラクション(ロールのメソッド)を定義します。

  • ロールをクリックして選択 → 下のコード枠に次に示したメソッドを入力するかコピペ → alt + s で accept


定義するメソッドは4つ(Lender の draw: と pay: 、AmountPie の increase: と decrease: )ですが、冒頭の「ロール名 >> 」より後がメソッドのコード本体なので、そこだけコピペ(あるいはタイプして入力)して「ロール名 >> 」の部分は含めないでください。Smalltalk 界隈では複数の異なるクラスに属するメソッドのコードを列挙して示す必要がある際に、そのメソッドがどのクラスのものかが区別できるようにそれぞれの頭に「クラス名 >> 」と書く慣習があります。ここではロールをクラスに見立てて、同様の記法を用いました。

たとえば Lender の draw: メソッドであれば、Lender >> の直後の draw: amount から3行目の limit - amount の行までをタイプして入力するか、選択してコピペしてください。その後の accept(alt + s) もお忘れなく。

f:id:sumim:20190223032837p:plain

Lender ロールへの draw: メソッドの定義
Lender >> draw: amount
	AmountPie increase: amount.
	self limit: self limit - amount
Lender >> pay: amount
	AmountPie decrease: amount.
	self limit: self limit + amount
AmountPie >> increase: amount
	PercentagePie shares associationsDo: [:assoc |
		| company percentage |
		company := assoc key.
		percentage := assoc value / 100 asFloat.
		self shares
			at: company
			put: (self shares at: company) + (amount * percentage)]
AmountPie >> decrease: amount
	| shares total |
	shares := self shares.
	total := shares sum asFloat.
	shares keysDo: [:company |
		| current |
		current := shares at: company.
		shares at: company put: current - (amount * (current / total))]

 

コンテクストにおけるロールへのデータマッピングメソッドの定義

コンテクストにおいて、どのロールをどんなオブジェクトに演じさせるのかを割り振るメソッドを定義しておく必要があります。

  • ロールを指さないように下地の部分でクリック → role binding methods
  • ブラウザが開くので、ロールと同名の三つのデータマッピングメソッドがリストされていることを確認。
  • Lender を選択し、コード枠内のコードを次のように書き換えて accept (alt + s)。

f:id:sumim:20190223034009p:plain

role binding methods

f:id:sumim:20190223034115p:plain

データマッピングメソッドをまとめたブラウザ
Lender
    ^facility
  • 同様に AmountPie、PercentagePie も定義を次のように書き換えて accept (alt + s)。
AmountPie
    ^facility loan
PercentagePie
    ^facility

各ロールへのデータマッピングメソッドの定義が終わったら Message Category Browser (BBLoanSyndicate) ウインドウは綴じてしまって構いません。

 

なお BabyIDE の場合、ロールの割り振りに相互依存性があったりしてその順序が重要な場合には、コンテクストの remap をオーバーライドしてデータマッピングメソッドのコール順を明示的にしておく必要があります。しかし、この例ではその必要はなさそうです。

各ロールにマウスをホバーすると、吹き出しにデータマッピングメソッドが表示されるので、以降は簡単に確認できます。

f:id:sumim:20190223034807p:plain

マウスホバーで現れる吹き出し(データマッピングメソッドの定義)

コンテクストへの各種メソッドの定義

BBLoanSyndicateCtx に初期化メソッド(#buildFacility、#joinFacility:percentage:)、および、トリガーメソッド(#draw: #pay:)を定義します。

  • SWAP TO CONTEXT ボタンをクリックして画面切り替え
  • 第三枠(カテゴリーリスト枠)で右クリック → new category... → initialization と入力してから Accept (alt + s)。
  • 追加された initialization をクリックして選択。
  • 次のそれぞれのメソッド定義を下段コード枠にペーストして accept (alt + s)。
buildFacility
	facility := BBFacility new.
	facility loan: BBLoan new
joinFacility: company percentage: int
	facility shares at: company put: int.
	facility loan shares at: company put: 0
  • 第三枠(カテゴリーリスト枠)で右クリック → new category... → triggers と入力してから Accept (alt + s)。
  • triggers をクリックして選択。
  • 次のそれぞれのメソッド定義を下段コード枠にペーストして accept (alt + s)。
draw: amount
	self triggerInteractionFrom: #Lender with: #draw: andArgs: {amount}
pay: amount
	self triggerInteractionFrom: #Lender with: #pay: andArgs: {amount}

 

f:id:sumim:20190223035901p:plain

コンテクストへのメソッドの追加

動作確認コード

以上で以上で定義は完了です。実際に動作を確認してみましょう。Data や Context と同様の方法で新しく Testing というタブを作り、そこに動作確認用の適当なクラスを定義して、クラスメソッドとして example メソッドを追加しましょう。

  • ウインドウタイトルバー右側にある青い▼ボタンをクリック して new projection... を選択、Testing と入力して Accept (alt + s)。(例によって、途中で出てくるノーティファイアによる警告は無視して Proceed で続行。また追加後、Data、Context を含む冗長な表記のタブがずらずら現れるので、set application... から BBLoanDyndicate を選択してリフレッシュ。
  • データモデルやコンテクストのときの要領で BBLoanSyndicateTesting クラスを Object のサブクラスとして定義。
Object subclass: #BBLoanSyndicateTesting
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'BBLoanSyndicate-Testing'

 

  • 次の example メソッドをクラスメソッドとして定義するため、クラスリスト枠(第二枠)下にある class ボタンをクリックして選択し、クラスメソッドサイドに切り替え。
  • メソッドカテゴリペイン(第三枠)から no messages をクリックして選択してから、次のコードを入力するかコピペして accept(alt + s)。
example
	"BBLoanSyndicateTesting example"
	| syndicate |
	syndicate := BBLoanSyndicateCtx new buildFacility.
	syndicate joinFacility: #A percentage: 50.
	syndicate joinFacility: #B percentage: 30.
	syndicate joinFacility: #C percentage: 20.
	syndicate facility limit: 100000.

	syndicate draw: 10000.
	syndicate pay: 5000.

	World findATranscript: nil.
	syndicate facility loan shares associations sort 
		do: [:assoc | Transcript cr; show: assoc]

コメントアウトしてある BBLoanSyndicateTesting example 部分を選択して、do it (alt + d) すると、トランスクリプトが前面に現れ、結果が出力されます。

f:id:sumim:20190223130214p:plain

example メソッドの実行

BBLoanSyndicate全ソースコード

青▼ → printHtml for this App で生成(一連のファイルは ./Squeak-4.5-All-in-One.app/Contents/Resources/BBLoanSyndicate-listing に出力)される、ソースコード閲覧用の HTML ファイルを下の場所に置いておきます。

Smalltalk-76 とアルト・エミュレータ(ContrAlto)でマンデルブロ集合

最後はやっぱりアルト実機に対抗(?)して、アルト・エミュレーターContrAlto で動く Smalltalk-76 で書いたマンデルブロ集合描画で締めくくりたい…と思ったのですが、想像以上に遅くてけっこう大変でした。^^;


Smalltalk-76 を動作させるにあたり こちら のディスクパック・イメージ xmst76.dsk44 を使用させていただきました。Smalltalk-76 の起動は resume xmsmall.boot です。


Smalltalk-76 は、Pharo や Squeak Smalltalk の元になっている Smalltalk-80 と似たところが多い(初期の Smalltalk-72 と比べればはるかに!)ですが、それでもいろいろと勝手が違うところもあるので「過渡期」と見るだけでなく「Smalltalk-80とは似て非なる独自の言語」として見ても興味深い処理系です。こちらの文書が参考になります。


ContrAlto Readme には raw packet を使いたいときだけのように書いてありますが、Microsoft Visual C++ 2010 redistributable は設定画面(System → System Configuration...)を正常に機能させるのにも必要ですのでネットに繋げなくとも忘れずにインストールしておきましょう。同設定から Displayタブの Throttle Framerate at 60 frame/sec のチェックを外しておくと手元の環境では気持ち快適に動作しました。あと、ホイールボタンが使用できる3ボタンマウスが必須です。


まずは、複素空間の座標を Point で表わす方法で素朴に書いてみたのがこちら。

http://squab.no-ip.com/collab/uploads/mandelbrot11.png

| size half origin turtle limit start x y c z count zsquared [
   size ← 10.
   half ← size/2.
   origin ← Rectangle new fromuser origin.
   (origin - 1 extent: size⌾size + 2) outline; clear: black.
   turtle ← Turtle init.
   limit ← 10.
   start ← user rawtotalsecs.
   for⦂ y from: (0 to: size-1) do⦂ [
      for⦂ x from: (0 to: size-1) do⦂ [
         c ← x asFloat ⌾ y asFloat - half / half - (0.5⌾0).
         z ← 0.0⌾0.0.
         count ← 0.
         while⦂ ((count ← count + 1) ≤ limit and⦂ z length < 2) do⦂ [
            zsquared ← z x * z x - (z y * z y) ⌾ (2.0 * z x * z y).
            z ← zsquared + c
         ].
         [count > limit ⇒ [turtle black] turtle white].
         turtle penup; place: x⌾y + origin; pendn; go: 0
      ]
   ].
   user rawtotalsecs - start
]


ポイント記号 ⌾ は ctrl + ] 、オープンコロン ⦂ は ctrl + ; 、小なりイコール ≤ は ctrl + , 、〜ならば ⇒ は ctrl + / で入力できます(US キーボードの場合)。


走らせてみたところ 10×10ドット(左下の小さいやつ!)で集合値判定ループも最大10回にまで減らしたにもかかわらず描き終えるのに 6分近くかかるという状態。この様子では全画面はおろか、100×100 でも、おそらく1日かけても終わりません。BCPL よりは遅いかな…程度に軽く考えていましたが、あまかった!


念のため、Squeak Smalltalk でほぼ同じ意味になるように書き換えたのがこちら。100×100ドット、判定ループ最大100回でも 0.2秒ほどで終わります。いい時代になりました。w

| size half origin turtle limit start c z count zsquared |
size := 100.
half := size/2.
origin := Rectangle fromUser origin.
Display border: ((origin extent: size asPoint) expandBy: 2) width: 2 fillColor: Color black.
turtle := Pen new.
limit := 100.
start := Time millisecondClockValue.
(0 to: size-1) do: [:y |
   (0 to: size-1) do: [:x |
      c := x asFloat @ y asFloat - half / half - (0.5@0).
      z := 0.0@0.0.
      count := 0.
      [(count := count + 1) <= limit and: [z r < 2]] whileTrue: [
         zsquared := z x * z x - (z y * z y) @ (2.0 * z x * z y).
         z := zsquared + c
      ].
      turtle color: (count > limit ifTrue: [Color black] ifFalse: [Color white]).
      turtle up; place: x@y + origin; down; go: 0
   ]
].
Time millisecondClockValue - start / 1000.0 "=> 0.211 "


そこで、Point の使用を諦め、主に型変換のコストがかからないように最低限の変更を加えてみたのがこちらです。

http://squab.no-ip.com/collab/uploads/mandelbrot09.png

| size half ori turtle limit start x y cre cim zre zim zre2 zim2 count [
   size ← 101.
   half ← size/2.
   ori ← Rectangle new fromuser origin.
   (ori extent: size asPoint) outline; clear: black.
   turtle ← Turtle init.
   limit ← size/3.
   start ← user rawtotalsecs.
   for⦂ y from: (0 to: half) do⦂ [
      for⦂ x from: (0 to: size-1) do⦂ [
         cre ← x asFloat - half / half - 0.5.
         cim ← y asFloat - half / half.
         zre ← zim ← zre2 ← zim2 ← 0.0.
         count ← 1.
         while⦂ (count ≤ limit and⦂ zre2 + zim2 < 4.0) do⦂ [
            zim ← 2.0 * zre * zim + cim.
            zre ← zre2 - zim2 + cre.
            zim2 ← zim * zim.
            zre2 ← zre * zre.
            count ← count + 1
         ].
         [count > limit ⇒ [turtle black] turtle white].
         turtle penup; place: x + ori x ⌾ (y + ori y); pendn; go: 0.
         turtle penup; place: x + ori x ⌾ (size - y - 1 + ori y); pendn; go: 0
      ]
   ].
   user rawtotalsecs - start
]


もうなりふり構っていられないので、判定ループもドット数(size)の3分の1程度の最大33回にして、さらにこの範囲での上下対称を利用して下半分も同時描画にしています。これでなんとか 9000秒 = 2.5時間程度で描き終わるようにできました。


さらに画面を 赤くする 消すと3倍くらい速くなるという情報が件のアルト実機でのマンデルブロ描画の続報にあったので、どうやらそれっぽい UserView>>#displayoffwhile⦂ を見つけて使ってみたところ、ほんとに速くなりました。

http://squab.no-ip.com/collab/uploads/mandelbrot10.png


まあ、描いている途中を見たいところもあるので微妙ですが。^^;


ということで、同シリーズはこれにておしまい。マンデルブロ集合はもうしばらく見たくないです。w



追記
id:squeaker さんからコメントをいただいたので、Lively Kernel 上にリバイブされた Smalltalk-78 でも試してみました。Smalltalk-78 はノートテイカー(NoteTaker)と呼ばれる 8086を搭載した可搬式 PC 試作機向けに機能を削減した Smalltalk-76 です。

ノートテイカーは、アルトのパワーアップ版のドラド(Dorado)に対し、小型化の方向でダイナブックに一歩近づいたアルト後継機に位置づけられるマシンで、後の有名なオズボーン1の元ネタでもあります(タッチパネルを装備したりバッテリー駆動が可能など、オズボーン1よりずっと高機能です^^;)。

前述エミュレーターは Web アプリフレームワークの Lively Kernel(Lively Web)上にノートテイカーのエミュを構築し、その上で Smalltalk-78 を動作させています。

JS で実装されているので個人的には Smalltalk ゆかりの高速化技術で作られた V8 エンジンをを搭載している Chrome を推奨しています。


http://squab.no-ip.com/collab/uploads/mandelbrot12.png

| size half ori turtle limit start x y cre cim zre zim zre2 zim2 count [
   size ← 101.
   half ← size/2.
   ori ← Rectangle new fromuser origin.
   (ori extent: size asPoint) outline; clear: black.
   turtle ← Turtle init.
   limit ← size/3.
   start ← user ticks.
   for⦂ y from: (0 to: half) do⦂ [
      for⦂ x from: (0 to: size-1) do⦂ [
         cre ← x asFloat - half / half - 0.5.
         cim ← y asFloat - half / half.
         zre ← zim ← zre2 ← zim2 ← 0.0.
         count ← 1.
         while⦂ (count ≤ limit and⦂ zre2 + zim2 < 4.0) do⦂ [
            zim ← 2.0 * zre * zim + cim.
            zre ← zre2 - zim2 + cre.
            zim2 ← zim * zim.
            zre2 ← zre * zre.
            count ← count + 1
         ].
         [count > limit ⇒ [turtle color: black] turtle color: white].
         turtle penup; place: x + ori x ⌾ (y + ori y); pendn; go: 0.
         turtle penup; place: x + ori x ⌾ (size - y - 1 + ori y); pendn; go: 0
      ]
   ].
   (user ticks - start) asFloat / 1000
]


こちらは ContrAlto と違ってコピペでコードを貼り付けできる(必要なら環境からコードをコピペで持ち出すこともできる)のが楽でいいですね。^^;

手入力したい場合は、オープンコロンは : の2連続 :: 、ポイント記号はそのまま @ 、小なりイコールは <= 、〜ならばは => で入力できます。


ちょっと手直してして動かしてみたところ 100×100ドット、最大判定ループ33回で 85秒ほどでした。速い!


これなら最初の素朴な実装も待てる時間で動くのでは?と同様の条件で試してみたところ、驚きの95秒。対称性を利用していないことを考えるとこちらの方が速いのかもしれません。すばらしいですね。

http://squab.no-ip.com/collab/uploads/mandelbrot13.png

| size half origin turtle limit start x y c z count zsquared [
   size ← 101.
   half ← size/2.
   origin ← Rectangle new fromuser origin.
   (origin - 1 extent: size⌾size + 2) outline; clear: black.
   turtle ← Turtle init.
   limit ← size/3.
   start ← user ticks.
   for⦂ y from: (0 to: size-1) do⦂ [
      for⦂ x from: (0 to: size-1) do⦂ [
         c ← x asFloat ⌾ y asFloat - half / half - (0.5⌾0).
         z ← 0.0⌾0.0.
         count ← 0.
         while⦂ ((count ← count + 1) ≤ limit and⦂ z length < 2) do⦂ [
            zsquared ← z x * z x - (z y * z y) ⌾ (2.0 * z x * z y).
            z ← zsquared + c
         ].
         turtle color: [count > limit ⇒ [black] white].
         turtle penup; place: x⌾y + origin; pendn; go: 0
      ]
   ].
   (user ticks - start) asFloat / 1000
]

Squeak Smalltalk でマンデルブロ集合 番外編(Squeak Etoys 毛玉版)

Squeak Etoys に組み込みの id:squeakerさんの毛玉(Kedama)を使って書いてみました。基本的に その3 などでやっているように実部と虚部に分けて計算する実装の移植です。毛玉は、端的に言うと LOGO の後継の StarLogo の流れを汲む並行処理版タートルグラフィックスシステムで、複数のタートルを同時に動かすことができるようになっています。

毛玉では複数のブリード(タートルの種類)を使いわけてグループごとに振る舞いを変えることができますが、今回は1種類 Breed1 しか使っていないのでこれはすなわちタートル(の振る舞い)そのものと考えて大丈夫です。

タートルというとその軌跡でグラフィックを描くイメージですが、ここでのタートルはあらかじめ 100×100 = 10000マスあるパッチ変数(セルのようなもの)に1匹ずつ割り当てておきそこからは動きません。では何をしているかというと、各々の複素平面内での位置(cre、cim)から z ^ 2 + c を繰り返し計算し、発散しない限りパッチ変数の数を減じてゆくよう振る舞わせています。発散したと判断されたときのパッチ変数の値がそのままグレイの点として描画に反映されます(早く発散してしまえば白のまま、発散しなければ黒に近づく)。

なお、Squeak EToys の四則演算は Smalltalk と同じく乗除優先がないのに加え、Smalltalk(ただし -76 以降)とは逆の右結合という特殊な振る舞いをするので注意が必要です。正しい演算結果を得るにはちょっとした工夫が必要になります。


毛玉についてはこちらの文書が一番詳しく書かれていると思います。

http://squab.no-ip.com/collab/uploads/mandelbrot08.png

Squeak Smalltalk でマンデルブロ集合 その3(1トゥート版)

マストドンでは1トゥートが 500文字までで、そのインスタンスのひとつの Qiitadon ではコードブロックを指定できることから、短めのプログラムならシンタックスハイライトを使ってトゥートすることが可能です。id:squeakerさんが提示された配列を積算する方法(特にコメントアウトされた素朴な実装の方)を見ていて、Squeak Smalltalk の持つ配列同士の演算を用いれば短く書くこともできるのではないかと思いついたのでトライしてみました。

http://squab.no-ip.com/collab/uploads/mandelbrot07.png


実はちょっと間違いがあって訂正済みの版がこちら。

|s h r x y c q m f i b|
s:=300.
h:=s//2.
s:=s roundUpTo:32.
r:=(0to:s-1)-h/h.
x:=(Array new:s withAll:r)concatenation.
y:=r gather:[:v|Array new:s withAll:v].
c:={x-0.5. y-0.0}.
q:=[:p||re im|re:=p at:1. im:=p at:2. {re*re-(im*im). re*im*2}].
m:=(1to:100)inject:c*0into:[:z :j|(q value:z)+c].
f:=Form extent:(h*2)asPoint.
i:=[:w|Integer readFrom:(w collect:[:v|(v<4)asBit asHexDigit])readStream base:2].
b:=(m*m)sum groupsOf:32atATimeCollect:i.
f bits:(b as:Bitmap);display


そしてスペース等の省略を戻し、少しだけ速度を意識して FloatArray 等若干の変更や拡張を施したのがこちらです。

| size half range xs ys cs squared time map form asInt32 bits |
size := 200.
half := size//2.
size := size roundUpTo: 32.
range :=  (0 to: size-1) - half / half.
xs := (Array new: size withAll: range) concatenation asFloatArray.
ys := (range gather: [:x | Array new: size withAll: x]) asFloatArray.
cs := {xs - 0.5. ys}.
squared := [:pair | {pair first squared - pair second squared. pair first * pair second * 2}].
time := [map := (1 to: 100) inject: cs * 0 into: [:zs :idx | (squared value: zs) + cs]] timeToRun.
form := Form extent: (half * 2) asPoint.
asInt32 := [:vs | Integer readFrom: (vs collect: [:v | (v < 4) asBit asHexDigit]) readStream base: 2].
bits := (map * map) sum groupsOf: 32 atATimeCollect: asInt32.
form bits: (bits as: Bitmap); display.
time. "=> 3033 "


ビットマップを生成してフォームに食わせているところがこだわりだったのですが、普通にピクセルごとに値を置いた方がずっと短く書けるのでなんだかなーと言う感じですね。^^;

|s h r x y c q m f|
s:=300.
h:=s//2.
r:=(0to:s-1)-h/h.
x:=(Array new:s withAll:r)concatenation.
y:=r gather:[:v|Array new:s withAll:v].
c:={x-0.5. y-0.0}.
q:=[:p||re im|re:=p at:1. im:=p at:2. {re*re-(im*im). re*im*2}].
m:=(1to:100)inject:c*0into:[:z :j|(q value:z)+c].
f:=Form extent:(h*2)asPoint.
(m*m)sum doWithIndex:[:v :i|f pixelValueAt:i\\s@(i//s)put:(v<4)asBit]. 
f display


ついでと言ってはなんですが、グレイスケール版にも挑戦してみました。もし #< や #asBit などが加減乗除などの二項セレクターと同様に Colletion のダブルディスパッチに対応していたら map := map + (zs squared sum asArray < 4) asBit などと速度はともかくもっと簡潔に書けそうなものなのですが、現状ではかなり見栄えの悪いものになってしまっていて残念です。

| size half range xs ys cs zs squared time map form bits |
size := 300.
half := size//2.
range :=  (0 to: size-1) - half / half * 1.3.
xs := (Array new: size withAll: range) concatenation asFloatArray.
ys := (range gather: [:x | Array new: size withAll: x]) asFloatArray.
cs := {xs - 0.75. ys}.
zs := cs * 0.
map := Array new: xs size withAll: 0.
squared := [:pair | {pair first squared - pair second squared. pair first * pair second * 2}].
time := [
   100 timesRepeat: [
      zs := squared value: zs + cs.
      map := map + (zs squared sum asArray collect: [:v | (v < 4) asBit]).
   ]
] timeToRun.
form := Form extent: (half * 2) asPoint depth: 32.
bits := map collect: [:v | (Color gray: 1- (v/100) sqrt) pixelWordForDepth: 32].
form bits: (bits as: Bitmap); display.
time. "=> 11591 "

http://squab.no-ip.com/collab/uploads/mandelbrot06.png


追記
最後に、速度を気にして FloatArray を使うなどしないのであれば、実部と虚部それぞれに配列を用意せずとも複素数の配列ひとつで済ませてもよいのではないかと書いてみたらこんな感じになりました。

| size half range cs form map time zs |
size := 300.
half := size//2.
range := (0 to: size-1) - half / half * 1.3.
cs := ((Array new: size withAll: range) + (range collect: #i) - 0.75) concatenation.
zs := cs * 0.
map := cs abs * 0.
time := [
   100 timesRepeat: [
      zs := zs squared + cs.
      map := map + (zs abs collect: [:v | (v < 2) asBit]).
   ]
] timeToRun.
form := Form extent: (half * 2) asPoint depth: 32.
map doWithIndex: [:v :idx | form colorAt: idx\\size@(idx//size) put: (Color gray: 1- (v/100) sqrt)].
form display.
time. "=> 15789 "

Squeak Smalltalk でマンデルブロ集合 その2(id:squeakerさん版)

コメントでいただいた id:squeakerさんのコードに僭越ながら少し手を入れたものを。FlaotArray>>#*= とか知らなくてなんじゃこれ?!となったのは内緒です。^^; Float nan や Float infinity の振る舞いを利用するのは面白いですね。

| size half zREs zIMs prevREs prevIMs cREs cIMs idx time |

size := 600.
half := size / 2.

zREs := FloatArray new: size * size.
zIMs := FloatArray new: size * size.

prevREs := FloatArray new: size * size.
prevIMs := FloatArray new: size * size.

cREs := FloatArray new: size * size.
cIMs := FloatArray new: size * size.

idx := 1.
half negated to: half - 1 do: [:y |
   half negated to: half - 1 do: [:x |
      cREs at: idx put: x / half.
      cIMs at: idx put: y / half.
      idx := idx + 1
   ]
].

cREs -= 0.5.

time := [
   1 to: 100 do: [:z |
      zREs replaceFrom: 1 to: zREs size with: prevREs startingAt: 1.
      zIMs replaceFrom: 1 to: zIMs size with: prevIMs startingAt: 1.

      zREs *= prevREs -= (prevIMs *= prevIMs) += cREs.
      zIMs *= prevREs *= 2.0 += cIMs.

      {prevREs. prevIMs} elementsExchangeIdentityWith: {zREs. zIMs}.

      "prevREs := zREs.
      zREs := (zREs * zREs) - (zIMs * zIMs) + cREs.
      zIMs := (zIMs * prevREs) + (prevREs * zIMs) + cIMs."

   ]
] timeToRun.

Display restoreAfter: [
   idx := 1.
   0 to: size - 1 do: [:y |
      0 to: size - 1 do: [:x |
         | re im |
         re := prevREs at: idx.
         im := prevIMs at: idx.
         idx := idx + 1.
         ((re isNaN not and: [re isInfinite not]) and: [im isNaN not and: [im isInfinite not]])
            ifTrue: [Display colorAt: x@y put: Color black]
            ifFalse: [Display colorAt: x@y put: Color white]
      ]
   ].
   [Sensor anyButtonPressed] whileFalse
].

time "=> 16442 vs 25594 "

http://squab.no-ip.com/collab/uploads/mandelbrot04.png