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

前回のエントリは思ったよりアクセスがあったので補足しておく。
リストを扱うブロックを作る場合、ブロック追加のメソッドの修正と、リスト処理を行うメソッドの追加だけでは十分ではない。その他に、デフォルト値としてリスト名を自動的に設定することと、要素の位置を表す引数に数値以外が指定された時の処置を行う必要がある。
その辺りは、前回も参考にした’replace item’を真似しながら対応していくことにする。

blockSpecsからたどる

出発点は、ScriptableScratchMorphのクラスメソッドであるblockSpecsである。
今回はワークスペースを使いながら作業を進めるので、まずデスクトップをクリックしWorldメニューからopen…を選びworkspaceをクリックしてワークスペースウィンドウを表示させる。
ワークスペースに以下を入力するかコピー&ペーストしたら、全体を選択した後でCommand+d(ALT+d)を押す。

Browser fullOnClass: ScriptableScratchMorph class selector: #blockSpecs.

すると当該メソッドを表示した状態でシステムブラウザが起動される。
メソッドの下段(コードペイン)をスクロールさせて下の方を表示させると、前回のエントリで追加した部分が現れる。

('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' 1)

ここで、’replace item’のブロックは、#setLine:ofList:to:というメッセージを送り、前回追加した’swap item’のブロックは、#swapLine:ofList:for:というメッセージを送ることを示している。
また、’replace item’のデフォルト値として、第一パラメータに1、第二パラメータに’list’、第三パラメータに’thing’が用意されている。
#setLine:ofList:to:のメッセージがどのメソッドから送られているかは、以下のメッセージ式で調べることができる。

Smalltalk browseAllCallsOn: #setLine:ofList:to:.

上のメッセージ式を選択してCommand+d(ALT+d)を押すと、#setLine:ofList:to:のメッセージを送っているメソッドの一覧が水色のウィンドウで示される。
senders of #setLine:ofList:to:
リストに表示されているのは以下の3つである。(説明の都合上、表示した順序と変えている)

ScriptableScratchMorph class blockSpecs
ScriptableScratchMorph defaultArgsFor:
CommandBlockMorph coerceArgs:

1つ目は先ほど表示していたScriptableScratchMorphクラスのblockSpecsメソッドである。残りのうち2つ目のScriptableScratchMorphクラスのdefaultArgsFor:がデフォルト値の設定に関係がある。

デフォルト値の設定

水色の上段でScriptableScratchMorphクラスのdefaultArgsFor:メソッドを選ぶと、下段にメソッドの内容が表示されるので見てみる。
かなり長いメソッドだがスクロールしていくと最後の方に以下のような部分がある。

#setLine:ofList:to: = sel ifTrue: [
  defaultArgs size >= 3 ifTrue: [
    defaultArgs at: 2 put: self defaultListName.
    defaultArgs at: 3 put: (defaultArgs at: 3) localized]].

上記がsetLine:ofList:to:つまり’replace item’のデフォルト値の設定に関するコードである。元のblockSpecsでは、デフォルト値が設定されており、何もしなければ設定された値が用いられる。しかし、実際にScratchを動かして’replace item’のブロックを使ってみると、第二パラメータのリストの名前は、定義済みのリストの名前に置き換えられている。また日本語で表示させた場合、第三パラメータの値は「thing」ではなく、「なにか」という日本語に置き換わっている。
上記のコードは、ブロックのデフォルト値の内容を変更する処理を行っている。3行目では第2パラメータのリスト名を置き換え、4行目では第3パラメータの値のローカライズを行っている。
‘swap item’に対しても同様の処理を行う。’swap item’ではswapLine:ofList:for:というメッセージを用いるので、上記のコードをコピー&ペーストして、内容を一部置き換える。また、’replace item’と異なり、第3パラメータは要素の位置なのでローカライズする必要はない。以上をふまえると以下のようなコードを追加することになる。

#swapLine:ofList:for: = sel ifTrue: [
  defaultArgs size >= 2 ifTrue: [
    defaultArgs at: 2 put: self defaultListName.

このように、第2パラメータのみ実際のリスト名で置き換えるようにしている。この3行を先ほどのsetLine:ofList:to:の後に追加し、Command+s(ALT+s)で保存すれば良い。

要素の位置をあらわすパラメータの自動変換の抑制

実はデフォルト値の調整だけではうまくいかない場合がある。’swap item’のブロックでは第1パラメータと第3パラメータに要素の位置を指定するが、そこには数値だけでなくanyやlast(日本語では「どれか」や「最後」)を指定することができる。
今の状態で要素の位置にそれらを指定すると予期したような動作をしない。これは、数値を期待するパラメータに対して、Scratchが自動的に数値へ変換してしまうことによる。’replace item’でそのようにならないのは、自動的な変換を止めているからである。
これらの処理を行っているのは、最初の方で調べたsetLine:ofList:to:の呼び出し元のメソッドの3つ目であるCommandBlockMorphクラスのcoerceArgs:メソッドが行っている。このメソッドを調べてみよう。

Browser fullOnClass: CommandBlockMorph selector: #coerceArgs:.

ワークスペースで上記を選択後、Command+d(ALT+d)を押せば、システムブラウザが現れる。その内容は以下のようなものである。

coerceArgs: argList
    "Answer an arugments array in which all arguments that should be numbers have been coerced to numbers if necessary."
    | args specialCommands numFlags |
    args _ argList asArray.
    specialCommands _ #(
        append:toList: deleteLine:ofList: getLine:ofList: insert:at:ofList: list:contains: setLine:ofList:to:
        lookLike: showBackground:
        playSound: doPlaySoundAndWait
        setVar:to:).
    (specialCommands includes: selector) ifFalse: [
        "ensure args are numbers where numbers are expected"
        numFlags _ self numberArgFlags.
        1 to: args size do: [:i |
            (numFlags at: i) ifTrue: [args at: i put: (args at: i) asNumberNoError]]].
    ^ args

このメソッドではブロックに与えられる引数が数値を期待している場合、引数の内容(文字列などの場合もある)を数値に変換するという処理を行っている。ただし、必ずしも数値でないものを期待するブロックもあるため、それらについては例外として数値への変換を行わないようにしている。
上記のメソッドでは、例外となるメッセージをspecialCommandsという変数で管理している。’replace with’では要素の位置として強制的に数値への変換を行わないようにするため、’replace with’に対応するメッセージであるsetLine:ofList:to:がその変数に含まれている。
‘swap item’も2カ所で要素の位置を指定しており、その扱いは’replace with’と同じであるので、同様にspecialCommandsに含める必要があるため、上記のコードは以下のように変更する必要がある。(specialCommandsの代入の部分だけ抜粋)

    specialCommands _ #(
        append:toList: deleteLine:ofList: getLine:ofList: insert:at:ofList: list:contains: setLine:ofList:to:
        swapLine:ofList:for:
        lookLike: showBackground:
        playSound: doPlaySoundAndWait
        setVar:to:).

上記の変更を加えた後、Command+d(ALT+d)を押して保存すれば、要素の位置としてlastやanyを指定できるようになる。