#はじめに
Squeak/Pharo Smalltalk には Colorクラスが組み込みであり、たとえば Color red などとすることで赤色オブジェクトを取り扱うことが可能です。
Color 同士は加減算が可能で、たとえば Color red + Color blue は Color magenta を、Color yellow - Color green は Color red を返します。もちろん 赤 + 緑 + 青 なら 白、すなわち Color white を返します。
Color red + Color green + Color blue "=> Color white"
ところで Squeak/Pharo には、配列などのコレクションに sum というメッセージを送信すると要素の合計を返す機構があります。
#(1 2 3) sum "=> 6 "
つまり、要素に赤、緑、青を持つ配列の sum なら白になるはずです。試してみましょう。
"Squeak" {Color red. Color green. Color blue} sum "=> Color white "
"Pharo" {Color red. Color green. Color blue} sum "=> Color cyan ?! "
Squeak はちゃんと白を返してきて問題ないのですが、Pharo はシアンだと言い張ります。Collection>>#sum の実装を見て原因を探りましょう。
Squeak と Pharo の Collection>>#sum の実装は異なる
print it した式の sum の部分をドラッグ、あるいはダブルクリックで選択して Squeak では alt/cmd + m、Pharo では ctrl + m をタイプすると #sum の実装の一覧を見ることができます。
Squeak では素直に #reduce: を使っているのに対し、Pharo ではなにやら複雑なことをやっています。
どうやら、レシーバーであるコレクション(この場合は配列)から適当な要素を取り出して(anyOne)、それを初期値(第二引数)として与えて #inject:into: を回しているようです。しかし、全要素を回してしまうと、初期値(最初に取り出して適当な値)の分だけ足しすぎるので最後にそれを差し引いて合計として返す、という作業をしていますが、この辻褄あわせが今回のような RGB の合計の例ではうまく動かず、シアンを返してしまう元凶となっているようです。
そももそコレクションに含まれる要素の型がわかっていれば、その型のゼロを #inject:into: の初期値として与えられるのですが、残念ながら Smalltalk ではコレクションの要素の型は(IntegerArray や PointArray など決まっている配列等もありますが…)原則として決まりません。そこで、苦肉の策として適当な値を初期値として与え、最後に帳尻を合わせたわけですがこれがいけませんでした。
Pharo の Collection>>#sum を手直しする(案1)
個人的な好みとしては、sum する要素にはできれば加算、つまり #+ だけを要求したいところなのですが、この実装では最後に減算、つまり #- を要求してしまっているのでもうこのもくろみは成り立ちません。であれば sample - sample でゼロを生成し #inject:into: の初期値にしてしまえばよいのではないでしょうか。
Collection >> sum
"This is implemented using a variant of the normal inject:into: pattern.
The reason for this is that it is not known whether we're in the normal
number line, i.e. whether 0 is a good initial value for the sum.
Consider a collection of measurement objects, 0 would be the unitless
value and would not be appropriate to add with the unit-ed objects."
| sum sample |
sample := self anyOne.
^ self inject: sample - sample into: [:accum :each | accum + each]
これで Pharo の sum も無事、Color white を返してくれるようになります。
{Color red. Color green. Color blue} sum "=> Color while "
Pharo の Collection>>#sum を手直しする(案2)
sum をこんな実装にするところを見ると、Pharo には #reduce: がないのかなと思いきや、ちゃんとあるようです。
#(1 2 3) reduce: [:a :b | a + b] "=> 6 "
ただ、ブロックの代わりにシンボルを与えることはできないようです。
#(1 2 3) reduce: #+ "=> MessageNotUnderstood: ByteSymbol>>argumentCount "
シンボルが argumentCount なんて知るかボケ、とかのたまっています。
せっかくなのでちゃんと動くようにしてみましょう。出てきたエラー表示(ノーティファイア)にある Create ボタンをクリックして Symbol に仮のメソッド(中身は self shouldBeImplemented )を作ります。
途中、プロトコルをどうするか訊かれるので(デフォルトの as yet unclassified のまま OK でもいいですが) accessing にしておきます。
引数の数をきちんと返す内容にして(ブロック換算なので self numArgs + 1)accespt (ctrl + s) してコンパイルし、Proceed ボタンをクリックして実行を続行します。
これで無事 6 が返ると思いきや、なんだか様子がおかしいです(#+ が返ってきてしまう)。手を抜かずにちゃんと処理の内容を読まないといけないようです。^^;
あらためて #(1 2 3) reduce: #+ を選択して今度は print it の代わりに debug it (ctrl + shift + d) をします。デバッガーのウインドウ上部にある Into ボタンを二度押して #reduceLeft: のコンテキストに到達したら、Through ボタンの連打で処理を眺めてゆきます。
余談ですが、この Pharo の #reduceLeft: 、やけに複雑なコードになっていると思ったらとんでもない機能が組み込まれているようです。冒頭のコメントにあるように、演算子(Smalltalk には演算子というが概念は無いので、より正確には二項レセクター)もレシーバーの要素として与えることが可能になっているんですね。こんな実装で誰が嬉しいのかいまいちわかりかねますが…。^^;
#(1 + 2 - 5) reduceLeft: [ :a :op :b | a perform: op with: b ] "=> -2 "
閑話休題。
注意深く調べていくと、#valueWithArguments: をコールするところで Symbol(ByteSymbol)をスルーして Object>>#valueWithArguments: をコールしてしまっているのがわかります。引数を無視して self を返しているので、これではたしかに妙なことになるはずです。
ということで、Symbol にも同義のブロックを評価するのと同じ振る舞いができるように #valueWithArguments: をちゃんと定義してあげましょう。
ctrl + o → (ctrl を押したまま)b でシステムブラウザを開き、ctrl + f →(ctrl を押したまま)c とタイプして Symbol を呼び出します。上段3つめのプロトコルリスト枠から evaluating をクリックして選択して、下のコードペインに次のメソッドを定義して accept (ctrl + s)します。(コピペするときは、Symbol >> の部分は不要です。念のため)
Symbol >> valueWithArguments: args
^args first perform: self withArguments: args allButFirst
これでめでたく #(1 2 3) reduce: #+ は 6 を返してくれます。
最後に、Collection>>#sum の実装を ^ self reduce: #+ に変えておしまいです。
ようやく {Color red. Color green. Color blue} sum でも Color white を返してくれるようになりました。