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の更新状況によってはうまく動かないかもしれません。適宜対処してください。^^;

[追記]macOS Catalinaで古いSqueakが動かせなくなったとは聞いていて、いつものセキュリティ周り強化の余波だろうと軽く考えていたのですが、今回のは文字通り古い32bitソフトの息の根を止める処置なので致命的でした^^; なんとか動かせないか2つほど方法を考えました。

ひとつは順当に仮想化技術を使う方法です。見よう見まねでDockerイメージを作ってみましたのでお試しください 。→https://hub.docker.com/repository/docker/sumim/babyide14-docker

もうひとつは squeak.js.org を使う方法で、Launcherの点線枠に .image と .changes、.sources を含むファイル一式(VM関連以外の─)をドロップインするとあっさり起動します。Chrome推奨です。ただ squeak.js.org のデモ自体がいつまで使えるか分からないので、早めにこれ自体をローカルで動かせるように動態保存しておく方がよいかもしれません(→GitHub - codefrau/SqueakJS: A Squeak VM in Javascript)。[/追記]

ともあれ、ダウンロードして展開するだけ(古いバージョンだと、最低限必要なファイルを知って揃える必要はありますが…)という手軽さと環境を汚すことなくインストール(不要になれば削除でアンインストール)できる点は 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 ファイル生成時にロールメソッドを列挙できない同根のバグも退治しておきましょう。同じようにワークスペースなどで「runtimerole」などとタイプした後、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 ファイルを下の場所に置いておきます。