「使わないと損をする 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

Squeak Smalltalk でマンデルブロ集合

こちらの件のレストアしたアルト実機で BCPL で記述したマンデルブロ集合を実行してみたら1時間かかったという話を読んでいて、そういえば自分でマンデルブロ集合を描いてみたことがなかったなぁ…と気付いたのでさっそく書いてみました。

本当は、かなり以前に Smalltalk-80 で見た実装(ウインドウに表示して、一部を矩形選択するとその部分が新しいウインドウとして表示される)を作ってみたかったのですが、勢いで書けてワークスペースでさくっと実行できる簡単なもので済ませてしまいました。駄目ですね…。^^;

Display restoreAfter: [
   | limit dispRect ratio complexArea mandelbrot newRect scale |

   limit := 128.
   dispRect := Rectangle center: Display center extent: Display extent // 2.
   ratio := dispRect extent / dispRect height.
   complexArea := -1.0@0.0 - (ratio * 1.4) extent: ratio * 2.8.

   scale := [:pt |
      complexArea topLeft + (((pt x - dispRect left) * complexArea width / dispRect width)
         @ ((pt y - dispRect top) * complexArea height / dispRect height))].

   mandelbrot := [:c |
      | z count |
      z := 0.
      count := 0.
      [z squared abs < 4 and: [count < limit]] whileTrue: [
         z := z squared + c.
         count := count + 1
      ].
      (count / limit) sqrt
   ].

   Display fillWhite: dispRect.

   [:exit | [
      dispRect top to: dispRect bottom - 1 do: [:y | dispRect left to: dispRect right - 1 do: [:x |
         | pos color |
         pos := scale value: x@y.
         color := Color h: (mandelbrot value: pos x + pos y i) * 360 + 120 s: 1.0 v: 1.0.
         Display colorAt: x@y put: color.
         Sensor anyButtonPressed ifTrue: [exit value]
      ]].

      newRect := Rectangle fromUser.
      newRect area > 0 ifFalse: [exit value] ifTrue: [
         | center delta |
         center := newRect center.
         delta := dispRect extent * (newRect width / dispRect width max: newRect height / dispRect height) / 2.
         complexArea := (scale value: center - delta) corner: (scale value: center + delta).
      ]
   ] repeat] valueWithExit
]


Squeak のワークペース(デスクトップメニュー or Tools → Workspace )にペースト(alt/cmd + v)して改めて全選択(alt/cmd + a)後、do it(alt/cmd + d)で実行できます。

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


一部を矩形選択するとその部分を拡大して再描画します。枠外を大きく選択すると縮小もできます。選択をせずクリックすると終了です。

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


Pharo の Playground でも動かせますが、なんとけしからんことに Squeak で組み込みだった Complex が外されてモジュールに追い出されてしまっているので事前にインストールしておく必要があります(デスクトップメニュー → Tools → Catalog Browser → complex などと入力して Complex を右クリック → install stable version )。

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


もうひとつ Pharo では描画後の拡大域選択操作の前にアクティブウインドウの再描画がされてしまうので、Playground ウインドウはあらかじめ端によけておかないといけません。ご注意あれかし。