これは「大石泉すき」 Advent Calendar 2019 16日目の記事です。私は趣味でたまにプログラミングを嗜む程度なので、他の参加者様方の高品質な記事に震えています。
この記事で何をするか
大石泉をクリックして "idol++" してもらうと、周りにいるオタクが反応して "大石泉すき" とつぶやくプログラムを Pharo (Smalltalk) で実装します。内容の深さは、記事を追って行けば皆さま自身の環境で同じプログラムが動く、という程度にし、動作の中身について詳しくは突っ込みません。ちなみに私は Pharo 初心者なのでそのつもりでお読みいただけますと幸いです。
内容を理解しながらこの記事を読まれたい方は、Smalltalk Advent Calendar 2014 の初心者向けの記事などを参照されながら読まれると良いと思います。
環境
- OS: macOS Catalina 10.15.1
- Pharo 7.0.4 (PharoLauncher)
Pharo についてと環境構成
Pharo とはオブジェクト指向の始祖と噂される Smalltalk の一方言のようなものです。Smalltalk の開発者らが立ち上げた Squeak というプロジェクトからフォークしたもので、実用に耐えうる Smalltalk を目指しているようです。余談ですが本家 Smalltalk 直系の環境 VisualWorks も存命です。
インストールとイメージの起動
まずは Pharo.org へ行きましょう。
https://pharo.org
ここから Pharo Launcher をダウンロードしてきて、アプリケーションフォルダに入れます。上手いこと起動させると次のような画面が表示されます。
Smalltalk 環境は仮想化よろしく、イメージファイルを VM (仮想マシン) に読み込ませることで起動します。
Pharo 7.0 - 64bit の上で右クリックし、create image を選択します。するとイメージの名前を入力するよう促されます。何を入力しても大丈夫ですが、ここでは IzumiSuki にしておきます。
次に環境を起動してみます。右側に現われた IzumiSuki を選択し、右上の緑色の矢印をクリックします。
すると次の画面が現われます。Pharo ではこの環境の中でプログラミングをして行くことになります。
Welcome スクリーンは閉じてしまってもかまいません。
日本語環境の設定
Pharo のデフォルトの状態では日本語を表示することが出来ません。このままではどうしようもないので、まずは日本語のフォントを設定します。
何もないところでクリックをし、World メニューを開きます。
Pharo -> Settings を開きます。すると Appearance に Standard fonts という項目がありますので、Default の Source Sans Pro Regular 10 ボタンをクリックします。右下の Update を押すとシステムフォントが読み込まれるので、日本語が表示出来るフォントを選択し、OK ボタンを押します。Settings ウインドウに戻ったら、先ほど選んだボタンの右側にある Force all ボタンをクリックします。これで Pharo で日本語が表示出来るようになりました。Settings Browser は閉じてしまってかまいません。
Pharo の終了方法
World メニューを開き、Pharo -> Save and quit を選択すると終了します。これは "そのままの状態" を保存してくれますので、休憩したくなったときなどはこれで終了しても、次回起動したとき続きからすぐ始めることが出来ます。
とりあえずの大石泉すき
Playground と Transcript
今回はグラフィックを使って "大石泉すき" しますが、実はシェルで実行するように文字を出力することもできます。まず Playground と呼ばれるウインドウを開きます。World メニューを開き、Tools -> Playground をクリックします。また同様に Tools -> Transcript をクリックし、Transcript ウインドウを開きます。
Playground に次を入力します。
Transcript show: '大石泉すき'.
これは 「Transcript オブジェクトに show: '大石泉すき' というメッセージを送る」という文です。マウスをドラッグしてこの行を選択したあと、右クリックで開いたメニューの Do it をクリックします。
Transcript ウインドウに "大石泉すき" が出力されました!
もう少し Pharo っぽく
既に大石泉すきが出力されましたが、さすがにこれではつまらないのでもう少し Pharo っぽくやってみます。Playground に次を入力して Do it します。
'大石泉すき' asMorph openInHand.
吹き出しを出すクラスを作る
ここまでで既にお腹いっぱいかもしれませんが、ここからが本番です。最初にお見せしたプログラムを作るためにクラスを定義して行きます。
さて、大石泉は超絶美少女アイドルで一方オタクは魑魅魍魎ですが、一応同じ人間なのでクリックをすると吹き出しを出す共通のスーパークラスを作ります。Pharo にはクリックしたときに見た目を変化させる機能を提供する SimpleSwitchMorph (下の gif)がありますので、そのサブクラスとして定義します。
最初のクラス定義
World メニューを開き(何もないところでクリック)、Tools -> System Browser をクリックします。クラスの定義は普通、この System Browser と呼ばれるウインドウで行ないます。既に大量のクラスが定義されていてめまいがしますが、まずは今回定義するクラスを入れるパッケージを作ります。
一番左のペインで右クリック、出てきたメニューから New package をクリックして適当な名前を入力します。ここでは IzumiSuki にしておきます。
クラスを定義するには、IzumiSuki パッケージを選択したあと下側のテキストボックスにクラスの情報を入力し、最後に Accept を行ないます。試しに次の文を入力してみて下さい。
SimpleSwitchMorph subclass: #IMChara
instanceVariableNames: 'msgBalloon message performing'
classVariableNames: ''
package: 'IzumiSuki'
Command+S もしくは右クリックでメニューを開いて Accept を選択するとクラスの定義が確定します。
これでクラスを定義することが出来ました。
少し内容を詳しく見てみますと、1行目はクラスの名前とどのクラスのサブクラスであるかを表わしています。今は SimpleSwitchMorph のサブクラスとして IMChara クラスを定義しました。2行目はインスタンス変数、3行目はクラス変数です。実際はあらかじめ定義しておかなくても、未定義の変数を使用したときに Pharo の方からインスタンス変数として定義するかを聞いて来ます。4行目はクラスが所属するパッケージ名です。
次にメソッドを定義します。左から3番目のペインにある instance side を選択します。すると下のテキストボックスがメソッド定義用のものに変化します。次の文を順番に入力し、メソッドの定義が終わるごとに Command+S もしくは右クリックでメニューを開いて Accept を選択し、確定させます。(1行目の IMChara>> はどのクラスのメソッドであるかを表わしているものなので入力しないでください。)
IMChara>> initialize
super initialize.
self label: ''.
self borderWidth: 0.
bounds := (0 @ 0) corner: (100 @ 100).
message := 'hello, world'.
msgBalloon := BalloonMorph string: message for: self.
self color: (Color transparent).
self useSquareCorners.
self turnOff.
initialize メソッドは他の言語と同じように、オブジェクトを生成したとき最初に呼ばれるメソッドです。ここでは bounds 変数に代入を行なうことでサイズを変更し、吹き出しを出すための Balloon を生成してインスタンス変数 msgBalloon に代入しています。また自分自身の色を透明にしています。
IMChara>> mouseDown: evt
IMChara>> mouseUp: evt
self setSwitchState: (performing not)
これらはマウスクリックのイベントで実行されるメソッドです。mouseDown はスーパークラスで定義されている動作を行なわないために定義しています。
IMChara>> message: aString
message := aString.
self refreshBalloon.
IMChara>> refreshBalloon
msgBalloon := BalloonMorph string: message for: self.
IMChara>> turnOff
performing := false.
msgBalloon delete.
IMChara>> turnOn
performing := true.
msgBalloon openInWorld.
基本的には SimpleSwitchMorph のメソッドをオーバーライドしています。新しいメソッドは Balloon 用です。
動作確認
ちゃんとクラスが定義出来ているか確認するため、一度表示させてみましょう。Playground を開きます(World -> Tools -> Playground を選択)。ただ initialize メソッドで IMChara の色を透明にしているため、このまま表示しても "何も見えないところをクリックしたら突然「こんにちは世界」と言ってくる謎の物体" になってしまいますので、表示させる前に色をいじります。
IMChara new.
行を選択し、右クリックから Inspect it を選びます。するとインスペクタが開き、ここからオブジェクトをいじることが出来ます。インスペクタウインドウ下のテキストボックスに次の通り入力し、行を選択したら右クリックし Do It します。
self color: (Color black).
すると上側に表示されている color の項目が黒に変化します。これでようやくオブジェクトを表示させても目に見えるようになりました。下のテキストボックスで次を Do It します。
self openInHand.
これでマウスポインタのところに黒い物体が現われたと思います。この物体は適当な場所でクリックすることでその場所に固定されます。一度クリックをして固定したらもう一度この黒い四角をクリックします。
これで世界にあいさつをする暗黒物質が誕生しました。しかしオタクはまだしも、大石泉はもちろん暗黒物質ではないのであとで大石泉のサブクラスを定義します。
LiveStage クラス
大石泉がオタクと繋がるためのライブステージを定義します。これも凝れば綺麗なライブステージが作れますが、今回は省エネでいきます。
システムブラウザを立ち上げ IzumiSuki カテゴリを選択したら下のテキストボックスに次を入力して Accept します。
Object subclass: #LiveStage
instanceVariableNames: 'performer listeners'
classVariableNames: ''
package: 'IzumiSuki'
performer に大石泉が、listeners にオタク達が入ります。
続いて次のメソッドを定義します。
LiveStage>> initialize
super initialize.
performer := nil.
listeners := OrderedCollection new.
LiveStage>> joinListener: anOtaku
listeners add: anOtaku.
LiveStage>> listeners
^listeners
LiveStage>> performer: anIdol
anIdol onStage: self.
performer := anIdol.
後で LiveStage を生成しやすくするために、クラスメソッドを定義しておきます。システムブラウザの中央くらいにある Class side を選択します。するとブラウザの表示がインスタンスメソッドからクラスメソッドへと切り変わります。この状態で次のメソッドを定義します。
LiveStage>> startLive: anIdol listeners: otakuList
| stage | "<= 一時変数"
stage := self new.
stage performer: anIdol.
otakuList do: [ :otaku | stage joinListener: otaku ].
^stage
定義が終わったら Class side から Inst. side へ戻しておきましょう。
Izumi クラスと Otaku クラス
Izumi クラスの定義
まずは Izumi クラスを定義しましょう。システムブラウザを立ち上げ IzumiSuki カテゴリを選択したら下のテキストボックスに次を入力して Accept します。
IMChara subclass: #Izumi
instanceVariableNames: 'figure stage'
classVariableNames: ''
package: 'IzumiSuki'
figure には大石泉の画像が、stage には LiveStage が入ります。続いて以下のメソッドを定義します。
Izumi>> initialize
super initialize.
figure := (ImageReadWriter formFromFileNamed:'./izumi.png').
bounds := (0 @ 0) corner: (figure width) @ (figure height).
self addMorph: figure asMorph.
self message: 'idol++'.
figure では画像の読み込みをしています。Mac でのカレントディレクトリは "~/Documents/Pharo/images/イメージ名" になりますので、ここに izumi.png を入れれば OK です。今回は文字で "大石泉" と書いた画像を使っていますが、もちろん大石泉自身の画像を入れれば大石泉が表示されます。
Izumi>> onStage: aStage
stage := aStage.
Izumi>> liveOnStage
self turnOn.
stage listeners do: [ :otaku | otaku genkai ].
Izumi>> setSwitchState: aBoolean
aBoolean
ifTrue: [self liveOnStage]
ifFalse: [self turnOff]. "Smalltalk の条件分岐はメッセージで行なわれる"
liveOnStage メソッドを turnOn メソッドの前に呼ぶようにします。liveOnStage メソッドは LiveStage を見ているオタクを genkai (限界)状態にするようにします。
Otaku クラスの定義
最後にオタクを定義します。
IMChara subclass: #Otaku
instanceVariableNames: ''
classVariableNames: ''
package: 'IzumiSuki'
Otaku>> initialize
| head body |
super initialize.
head := PolygonMorph vertices: { 0 @ -10. 20 @ 10. 0 @ 30. -20 @ 10 }
color: Color black
borderWidth: 0
borderColor: Color transparent.
head beSmoothCurve.
self addMorph: head.
body := PolygonMorph vertices: { 0 @ 0. -25 @ 50. 25 @ 50 }
color: Color black
borderWidth: 0
borderColor: Color transparent.
self addMorph: body.
bounds := (-25 @ -10) corner: (25 @ 50).
self message: '大石泉すき'.
オタクにも姿形があるので、initialize メソッドでコナンの犯人のような姿を描くようにしています。
Otaku>> genkai
self turnOn.
限界状態のオタクはスイッチが入った状態になります。
大石泉すき
準備
Playground (World -> Tools -> Playground) を開き、まず次の4行を入力、選択し Do it します。
izumi := Izumi new.
otakus := { Otaku new. }.
stage := LiveStage startLive: izumi listeners: otakus.
izumi openInHand.
大石泉がマウスカーソルに付いて来てくれますので、どこかで適当な場所でクリックして固定します。そして最後に次を Do it してオタクを暗黒召喚します。
(otakus at: 1) openInHand.
実行
大石泉をクリックすると、idol++ の吹き出しと一緒にオタクが限界を迎え、大石泉すきとつぶやきます。
おわりに
長々とお読みいただきありがとうございました。もし Smalltalk に興味を持たれた方がいらっしゃれば、是非色々記事を探してみてください。Pharo は昨今のマイナープログラミング環境には珍しく、豊富なドキュメントが備わっています。
今回は Morph でグラフィックをいじりましたが、最近は Spec というフレームワークが登場し移行が進んでいるようです。(詳しくは Smalltalk Advent Calendar 2019 の razdan3 さんの記事をお読み下さい。)
さて、そろそろモバマスで大石泉が登場するはずなので、みんな震えて眠りましょう... 大石泉すき...