Scratch 1.4でブロックを追加するチュートリアル

授業でアルゴリズムを教えるのに普通のプログラミング言語を使わない方法を探していて、Scratchでいいじゃんと思ったけど、Scratchだと簡単なアルゴリズムを記述するのに多くのブロックが必要だったりするので、かえって面倒くさくなる。
そこで、簡単なソートアルゴリズムを実装するのに有用なブロックをScratchに追加することにした。
そもそもScratch 2.0を使えば自由にブロックを作れるが、ここではSmalltalkerらしくScratch 1.4を改造してブロックを追加する方法を示す。

簡単に説明しようと思ったら、かなりの大部になったのでチュートリアルとして公開することにした。おそらく今の時点でこれを必要とする人はいないだろうが、備忘録として残しておく。

準備

Scratch 1.4のダウンロードとインストール

まずScratch 1.4をインストールしておく。
Scratch 1.4はここから自分の環境にあったものをダウンロードする。
ダウンロードしたら展開して、素のScratchが起動することを確認しておく。細かい手順は省略。

Scratch 1.4のソースのダウンロード

次にScratch 1.4のソースコード(いわゆるBased on Scratchのイメージ)を手に入れる。
ソースコードはこのページの中程にあるScratchSource1.4.zipのリンクからダウンロードする。
ダウンロードしたら展開して、先ほどScratchを展開したフォルダにScratchSourceCode1.4.imageとScratchSourceCode1.4.changesをコピーしておく。

Based on Scratchの起動

ScratchSource1.4に含まれるイメージファイル(ScratchSourceCode1.4.image)でScratchを起動する。
おおむねどの環境でも、ScratchSourceCode1.4.imageを、Scratchの実行形式にドラッグ&ドロップすれば起動できるはず。
起動すると下の画面のように、いつもはScratchと書かれる左上の場所がBased on Scratchに変わり、Scratch catが顔の見えない猫の背中に変わっている。

ScratchSourceCode1.4の起動画面
ScratchSourceCode1.4の起動画面

Scratchの画面の裏には肌色と緑色の2つのウィンドウが見えている。肌色の方がこのイメージの使い方などが書かれたワークスペース、緑色の方がプログラムを修正するシステムブラウザである。

なお、ウィンドウを操作する場合、タイトルバー部分をクリックすることで手前に表示させることができる。Scratchの画面にはタイトルバーらしきものはないが、タイトルバーがあるべき付近をクリックすれば手前にもって来れる。

ブロックの定義

緑色のウィンドウのSystem Browserと書かれたタイトルバーをクリックするとシステムブラウザが手前に表示される。
ちゃんとタイトルバーをクリックしていれば、Scratchに新しいブロックを追加するヒントが表示されているはず。

System Browser画面

上の画面のように、ScratchSpriteMorphクラスのblockSpecsというクラスメソッドのソースが表示されているだろう。
この画面をよく見るとこんなことが書かれている。

blocks _ #(
    'motion'
        ('move %n steps'        -  forward:)
        ('turn %n degrees'      -  turnRight: 15) "icon shows turn direction"

これは、動き(motion)のカテゴリのコマンドを定義しており、3行目と4行目を見ると「○歩動かす」(move %n steps)と「○度回す」(turn %n degrees)のブロックを1行につき1つずつ定義していることがわかる。つまり、ここを書き換えることで動きカテゴリのブロックを追加したり修正できる。

(move %n steps)の行について正確に見て行こう。この行はカッコで囲まれた中に以下の3つの要素が含まれている。すなわち、「’move %n steps’」と「-」と「forward:」だが、「ブロックの内容」「フラグ」「メッセージセレクタ」の順に並んでいる。フラグの「-」はフラグ無しを表し、メッセージセレクタ「forward:」に付けられた「:」は、メッセージがキーワード付きメッセージであることを表している。
ようするに、「’move %n steps’」というブロックは「forward:」というキーワードメッセージを(引数付きで)送ることが表されている。

forward:のメソッド定義は、ScratchSpriteMorphクラスのインスタンスメソッドとして実装されている。System Browserの中ほどにあるinstanceというボタンを押すと、インスタンス側が表示される。上段左から3番目のペイン(メソッドカテゴリペイン)の中からmotion opsを選ぶと、左から4番目のペイン(メソッドペイン)にforward:が現れる。forward:をクリックすると、メソッドの内容が下段に表示される。(下図)

forward:のメソッド定義

ブロックの追加

動きカテゴリのブロックを追加するなら、上記の場所で何とかすれば良い。
今回はアルゴリズムの学習用なので変数やリストに関するブロックを追加したい。
具体的には、リストの1番目の要素と5番目の要素を交換するように、リストの2カ所の要素を交換するブロックを追加したい。
追加するブロックは、「swap item (位置) of (リスト) for item (位置)」のようなものとする。日本語なら「(リスト)の(位置)番目と(位置)番目の要素を交換する」みたいな。

先ほどのblockSpecsには、リストに関するブロックの定義が見当たらない。どうするか?
ここではどこかのクラスで同じようなメソッドがあり、そこでリストに関するブロックが定義されているだろうと当たりを付けて調べてみることにする。
まず、最初のblockSpecsのメソッドに戻る。(つまりinstanceの右のclassボタンを押し、メソッドカテゴリでblock specsを選んで、blockSpecsメソッドを選ぶ)
メソッドペインのblockSpecsをクリックするとメソッドペインの左側にスクロールバーが現れる。
スクロールバーの上ボタンの上にマイナスのマークの小さなボタンがあるので、これをクリックする。
すると以下のようなメニューが現れる。

メソッドペインのメニュー

このメニューからimplementors of …を選ぶと、blockSpecsを含むポップアップメニューが現れるのでblockSpecsを選ぶと、下のようなウィンドウが表示される。

Implementors of blockSpecs

これはblockSpecsメソッドを定義しているクラスの一覧である。ここでは3つのクラスで定義されており、そのうちの一つ(一番上)に当然ながらScratchSpriteMorphが含まれている。
他のエントリを見ると、ScratchStageMorphとScriptableScratchMorphの2カ所で定義されていることがわかる。
ScratchStageMorphは、ステージに関するブロックの定義があり、ScriptableScratchMorphではリストも含む様々なブロックが定義されている。
つまり、ScriptableScratchMorphクラスのblockSpecsを修正すれば良いことがわかる。

Implementorsの画面では後の操作が面倒なので、System Browserで表示させることにする。
Implementorsの上段でScriptableScratchMorphの行を選んだら、左側のスクロールバーの上の「-」ボタンを押してメニューを出し、一番上のbrowse fullを選ぶ。そうするとScriptableScratchMorphクラスのblockSpecsが選ばれた状態で、新たなSystem Browserが現れる。

browse full

メソッド定義を下の方にスクロールさせるとリストカテゴリのブロック定義が現れる。

リストのブロック定義

‘replace item’の定義が使えそうなので、それを参考に同じようなエントリを追加してみる。(2行目のみ追加する)

('replace item %i of %L with %s'  -  setLine:ofList:to: 1 'list' 'thing')
('swap item %i of %L for item %i'  -  swapLine:ofList:for: 1 'list' 2)

このブロック定義は先ほどのforward:より若干複雑である。
元の’replace item %i of %L with %s’には、%i、%L、%sの3つのパラメータが含まれており、これらは順に整数、リスト、文字列の引数を受け取ることを表している。
‘swap item’ではリストと2カ所の要素の位置を指定したいので、%i、%L、%iの3つをパラメータにする必要がある。

‘replace item’のメッセージセレクタはsetLine:ofList:to:となっている。これは3つの引数を取るキーワードメッセージを表しているが、これらの引数は先ほどのパラメータとそれぞれ対応する。
‘swap item’でも同様に、swapLine:ofList:for:というメッセージセレクタとし、位置、リスト、位置の3つの引数を受け取るようにする。

エントリを追加したら、Command+S(Windows/LinuxならAlt+S)で修正を保存しておく。

ブロックに対応するメソッドの追加

setLine:ofList:to:メソッドの定義を探す

新しいブロックを定義できたら、そのブロックに対する処理を行うメソッドを追加する。forward:と同様に、リストを処理するメソッドは、ScriptableScratchMorphクラスのinstance側にあるので、System Browserのinstanceボタンを押してインスタンス側を表示する。

次にメソッドカテゴリペインからlist opsを選ぶと、メソッドペインの一番下にsetLine:ofList:to:が見えるのでクリックする。

setLine:ofList:to:

画面のテキストを抜き出してみる。

setLine: lineNum ofList: listName to: anObject
  | list |
  list _ self listNamed: listName ifNone: [^ ''].
  ^ list setLineAt: (self lineNum: lineNum forList: list) to: (self asListElement: anObject)

(System Browserの画面では下線(_)が左矢印(←)に見えるが、これは代入のための記号をあらわしている)

setLine:ofList:to:のメソッド定義の1行目では、「:」の後に3つのパラメータ(lineNum, listName, anObject)が付与されており、それぞれ要素の位置、リストの名前、格納する値を表している。
2行目は一次変数listが宣言されているが、次の行でこの変数に実際のリストオブジェクトを格納する。
3行目では、listNameを持つリストを探し、あればlistに格納し、なければ空の文字列が返されている。
4行目を詳しく説明する。
(self lineNum: lineNum forList: list)では、listのlistNum番目の本当の位置を求めている。listNumには数値だけでなく、lastなどの文字列が入る場合があるので、それらに応じた適切な値を計算している。
(self asListElement: anObject)では、リストに格納するのに適した値に変換している。’swap item’では使わないので詳細は略。
list setLineAt: (…) to: (…)では、リストの指定された位置へ実際に値を格納する。

リストの要素を得るには?

‘swap item’では、リストの指定された位置の要素と、別の位置の要素を入れ替えるような内容にしなければならない。
そのためには、リストから指定された位置の要素を得る方法を知る必要がある。どこを探すべきか?
ソースを見ただけではlistのクラスはわからないので、listに送られるメッセージsetLineAt:to:から推測する。

メソッドペインにマウスを移動してスクロールバーの上の「-」ボタンをクリックし、implementors of …を選ぶ。
ポップアップメニューからsetLineAt:to:を選ぶと、新しくImplementors of setLine:ofList:to:のウィンドウが現れるが、ScratchListMorphというクラスでしか実現されていないので、これがlistに格納されるオブジェクトのクラスだとわかる。

Implementors of setLineAt:to:

Implementors of setLine:ofList:to:のウィンドウで上段の「-」ボタンをクリックしてbrowse fullを選ぶと、ScratchListMorphのSystem Browserが現れ、そのメソッドペインにはlineAt:というメソッドが見え、これがリストから指定された位置の要素を得るメソッドである。

lineAt:

swapLine:ofList:for:の追加

今までのことを総合すれば、目的のswapLine:ofList:for:は以下のように定義できる。

swapLine: lineNum ofList: listName for: lineNum2 
  | list i1 i2 v1 v2 |
  list _ self listNamed: listName ifNone: [^ ''].
  i1 _ self lineNum: lineNum forList: list.
  i2 _ self lineNum: lineNum2 forList: list.
  v1 _ list lineAt: i1.
  v2 _ list lineAt: i2.
  list setLineAt: i1 to: v2.
  ^ list setLineAt: i2 to: v1

listには、listNameで指定されたリストのオブジェクトを格納し、i1, i2には2カ所のリスト中の位置を格納する。そして、それぞれの位置の要素を取り出してv1, v2に格納した後、リストの2カ所の位置に(v2をi1へ、v1をi2へ)要素を入れ替えて格納する。

上記のコードをScriptableScratchMorphに登録するには、ScriptableScratchMorphのsetLine:ofList:to:を表示したSystem Browser画面に戻る必要がある。
System Browserの下段(コードペイン)で、setLine:ofList:to:のコードを上のswapLineのコードで全て置き換える。(コピー&ペーストでOK)
その後、Command+S(ALT+S)を押せば、イニシャルの入力を求められた後、ScriptableScratchMorphに登録される。

swapLine:ofList:for:

作成したブロックを試してみる

途中で開いた諸々のウィンドウを閉じて、Scratchの画面を表示させる。
Variablesのボタンを押してカテゴリを切り替え、Make a listボタンを押して適当な名前(例えばa)のリストを作成する。

make a list

すると、replace itemの下にswap itemのブロックが作られていることがわかる。

swap item

動作を確認するために下のようなスクリプトを作ってみる。

sample

上の固まりはリストaに10と20を追加するもので、下は作成した1番目と2番目の要素を入れ替えるブロック。上の固まりをクリックして実行するとリストに2つの要素が入り、下のブロックをクリックすると2つの要素が入れ替わる。

デフォルト値の設定

実はこの状態だとswap itemのブロックだけ、リストの名前が初期値としてセットされない。これを解決するには、defaultArgsFor:メソッドの内容を書き換える必要があるが、(もう眠いので)ここではその方法は示さないでおく。
(2014.4.11追記:この方法に関する記事はこちら

以上でブロックを追加するチュートリアルは終わり。

保存して終了

今までの変更を保存するのであれば、デスクトップをクリックして現れるWorldメニューからsave and quitを選べばよい。

save and quit

Scratchのように開発画面を表示せずに使いたいときは、シフトキーを押しながらScratchのFileメニューを押し、Save Image in User Modeを選べばよい。

Save Image in User Mode

それで…

Scratchで基本的なアルゴリズムが書きやすくなったのかというと、どうでしょうね。あまり変わらないような。