「使わないと損をする Model-View-Controller」(Smalltalkの古典的MVCの解説)のサンプルコードを SqueakJS のデモ画面(Mini Squeak 2.2)で動かす
「使わないと損をする Model-View-Controller」 はオブジェクト指向や Smalltalk関連の著作の多い青木淳氏による(今となっては)古典的な Smalltalk の MVC の解説記事で、ごく初期の“コントローラーが頑張るMVC”、そこから少し進化した“依存性を利用したMVC”、更に拡張性を意識した“プラガブルを利用するMVC”の三つに焦点を当て、シンプルなコードを示しながらそれぞれのポイントが簡潔に語られていて参考になります。
MVC のルーツについての理解を深める目的であれば、せいぜい書面でコードを追うだけでもよさそうなものですが、せっかくならやはり実際に動かしてみたい…と思うものですよね(え?思わない?^^;)。とはいえ(ともあれ)、当該文書掲載の Smalltalk のコードをそのまま写経できそうな商用の処理系の入手はほぼ不可能なので、オープンソースの Squeak Smalltalk のごく初期の実装(バージョン 2.2。1998年頃リリース)向けに少しだけいじって動かしてみました。(なお参考まで、同様の試みは Smalltalk勉強会 第30回 でもなされています。このときは Squeak1.31 が用いられました。)
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 はその名の通り JavaScript で Squeak Smalltalk の VM を実装することで、Webブラウザで手軽に Squeak環境を動かしてみたいときに使える便利な処理系です。また、Mini Squeak はメモリの少ないモバイル機器などでも動かせるように最低限の機能にシュリンクしたもので、かなり重要な情報や機能がごっそり削られてしまっているものの、非常にお手軽に SqueakJS サイトでワンクリックで起動できること、MVC に限っては当該サンプルコード程度なら問題なく動かせるので選びました。なお、SqueakJS それ自体の能力としては、特にシュリンクが必須ということはなく、比較的最近のバージョンの Squeak でもフルイメージのまま動かせます。念のため。
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ではさらにペイン寄りの場所でメニューポップアップも可能(ワンボタンマウス対策)。
ちなみに、Smalltalkでは第一マウスボタンを赤ボタン(red button)、第二マウスボタンを黄ボタン(yellow button)、第三マウスボタンを青ボタン(blue button)と呼び、コードでもそう表現します。中ボタンはともかく、現在のように「左ボタン」「右ボタン」などと表現しなかったのは、XEROX Alto時代、マウスのボタンの並びを横並びに限定することを避ける(実際にボタンを縦並びにしたマウスも試された)ためです。
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つのファイルのインポート
- Mini Squeak デモを開く直リン(ポータルの Mini ボタンでも OK) → https://squeak.js.org/demo/simple.html#fullscreen
Mini Squeak 2.2 が起動すると、ブラウザ内に Squeak 環境が表示されます。たとえば、「Welcome to Mini Squeak 2.2」ウインドウ内の 100 factorial をドラッグして選択してから 右クリック → print it (p) すると 100 の階乗が計算され結果が直後に挿入されます。
IDEのターミナルペイン内も含め、コマンドラインインターフェースでの言語処理系利用に慣れていると少々戸惑われるかもしれませんが、これが Smalltalk における典型的なコード実行スタイルです。カット&ペースト(ポップアップメニューを使ったものは Smalltalk が最初)と同様のワープロ内操作感覚で、選択した式などのコード片を気軽に評価(do it、print it)できるのが特徴です。
先ほど展開した MVC-Study.mini.st と Mickey.form をデスクトップ(Webページ)にドロップインします。何もフィードバックはありませんが、デスクトップクリック → open ... → file list で開く、簡易ファイラ兼エディタの右上のファイル一覧ペイン内にインポートされたファイルを確認できます。
ちなみにこれらのファイルはブラウザのローカルストレージに保存されてます。(不要になったら各ブラウザのツール、たとえば Chrome なら その他のツール → デベロッパー ツール → Application → Clear storage → Clear site data で削除できます。Squeak環境内で変更を加えたり、新しく作ったファイルは適宜 https://squeak.js.org/run/ ページの太枠点線内の一覧からダウンロードできます。)
▼MVC-Study.st のファイルイン(コードの読み込み)
そのまま file list のファイル一覧から MVC-Study.mini.st をクリックして選択し、右クリックメニューから file in します。
デスクトップクリック → open... → browser でシステムブラウザ(クラスブラウザとも呼ぶ)を開き、上段左端のペイン(枠)を最後までスクロール(マウスボタンをペイン内に入れてフォーカスし、左側にフリップアウトしたスクロールバーのスクロールボックスを下までドラッグ)して、MVC-Study が追加されていれば file in(コードの読み込みと一括コンパイル)は成功です。
▼Mickey1 class>>example(Mickey1 のサンプルコード)の実行
そのまま、上段第二ペインより Mickey1 → その下の class ボタンをクリック(クラスメソッドへの切り替え) → 第三ペインから examples → 第四ペインより example を次々にクリックして選択して下段のコードペインに Mickey1 class>>example メソッドのコードを呼び出します。
コード内のコメント "Mickey1 example"
のダブルクオーテーションで括られた中身(Mickey1 example
)だけを選択し(ドラッグしてもよいですが、ダブルクオーテーションの内側、つまり、最初のダブルクオーテーションのすぐ右側か、行末のダブルクオーテーションのすぐ左側をダブルクリックすると簡単です)、右クリック → do it (d) します。
すると、Mickey1 ウインドウが表示されます。ウインドウ内で右クリックすると magnify、shrink メニューがポップアップするのでいずれかを選択するとコードされたとおり、ネズミ(?)の絵が二倍に拡大(magnify)か、半分に縮小(shrink)されて表示されます。
▼変更個所と追加したサンプルコード(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
どちらのペインを操作しても、他方が同時に更新されるのを確認してください。
モデルの変更がそれを参照しているビュー(複数可)に自動的に伝播するのは、依存性を利用したMVC(ここでの例ではMickey2とMickey3)のキモですね。
ここで「複数可」と書き添えましたが、この振る舞いはビュー・コントローラーを介さずに、直接モデルを操作した場合でも同様です。たとえば、二つ目のコメントの MODEL magnify
を do it (d) すると Mickey2 ウインドウ内での操作を介さずにビューを更新できます(Mickey2 ウインドウが他のウインドウの裏にある場合、前面のウインドウの内容も重ね書きされてしまうのはご愛敬。デスクトップクリック → restore display で直せます)。
あくまでモデルは(コードの上では)ビューのことを知らない、というのがミソですね。
▼回転機能(rotate)を追加してみる
同文書にあるように Mickey3 に回転機能(rotate)を追加してみましょう。
まず、画像を回転するのに使えそうなメソッドがあるか探します。適当な場所(デスクトップクリック → open... → workspace でワークスペースを新たに開くなどして)で rotate などとタイプして入力してから選択後、右クリック → more... → selector containing it (W) を選ぶと、メソッド名(セレクター)に「rotate」を含むメソッドの一覧が表示されます。
Form>>rotateBy:centerAt: が使えそうなので、これで rotate 機能を実装します。まず、システムブラウザの class がハイライトしていたら instance ボタンをクリックしてインスタンスメソッド閲覧モードに戻してから、Mickey3 → manipulation をクリックして選択し、下のコードペインに次のコード(Mikcey3 >>
以降)をタイプして入力します。
Mikcey3 >> rotate form _ form rotateBy: #right centerAt: 0@0. self changed: #rotate
入力を終えたら 右クリックメニュー → accept (s) してコンパイルします。
なお初回のコンパイル時には、バージョン管理の際に記載するイニシャルを尋ねられるので何か適当に入力して enter してください。
コンパイルが通ると、上段右端のメソッド名一覧ペインのリストが更新され、rotate が追加されます。
続けて、上段第三ペインの accessing → 第四ペインの menu を選択し、下のコードペインのメニュー項目(selections: の引数の配列)に rotate を追加します。入力を終えたら accept (s) でコンパイルも忘れずに。
これで rotate の追加は終わりです。Mickey3 flushMenus; example
を do it (d) するとメニューに rotate が追加され機能するはずです(flushMenus;
はメニュー内容を更新後、初回のみ必要です)。
▼おまけ
Form-simpleDraw.mini.cs は、Mickey.form に自分で描いた絵を使いたい方向けのコードです。file list などを使ってファイルインして Form simpleDraw
を do it すると、カーソルが「+」に変わるので適当な余白に絵を描いてください。シフトキーを押すと描画を終了するので、Mickey.form の上書きするの確認(overwrite the file を選ぶ)に答えた後、引き続き左上から右下に向けてドラッグして矩形範囲を選択すると Mikcey.form の内容を選択して指定した矩型範囲(に描かれた絵)で置き換えることができます。
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