Help us understand the problem. What is going on with this article?

Squeak/Pharo Smalltalk に備わる APL/J ライクな配列演算機能

More than 3 years have passed since last update.

はじめに

Squeak や Pharo の配列には、簡易な演算機能が備わっています。たとえば、

#(1 2 3) * 4

を評価(print it)すると、レシーバーの #(1 2 3) の各要素をを 4 倍した #(4 8 12) を返します。配列ベースの言語である APL や J 、あるいは普通の言語で、たとえば Python の numpy などの数値計算ライブラリとして提供されているものほど本格的なものではありませんが、ちょっと似ていますね。

参考: Rubyist のための他言語探訪 【第 12 回】 APL と J

この配列にからめた演算機能、個人的にはよく使うお気に入りの機能なのですが、残念ながら Smalltalk 処理系ならどれにもある機能というわけではなないようです。これまで思い込みでてっきり Squeak の前身の Apple Smalltalk 時代に導入された機能だと考えていたのですが、この記事を書くにあたり改めて調べてみたところ、Squeak が 2.3 のときに組み込まれたもののようです。ともあれそんな経緯から当該機能は、現行の Squeak と、途中から派生した Pharo のみが有する Sqeuak/Pharo 系独自の機能ということになります。

ちなみに Sqeuak/Pharo 以外の普通の Smalltalk でこうした式を評価するとどうなるかというと、そもそも Collection>>#* が定義されていないので普通にエラー(MessageNotUnderstood 例外)になります。

配列に関してどんな演算ができるか?

使える二項演算は Collection の arithmetic プロトコル(aka メソッドカテゴリー)に属するメソッド群がそれに相当します。具体的には #* #+ #- #/ #// #\\ 。

"Squeak" (Collection allMethodsInCategory: #arithmetic) select: #isBinary
"Pharo" (Collection allSelectorsInProtocol: #'*Collections-arithmetic') select: #isBinary
=> #(#* #+ #- #/ #// #\\)
#(1 2 3) + 4  "=> #(5 6 7) "
#(1 2 3) - 4 "=> #(-3 -2 -1) "
#(1 2 3) / 4 "=> {(1/4) . (1/2) . (3/4)} "
#(1 2 3) // 2 "=> #(0 1 1) "
#(1 2 3) \\ 2 "=> #(1 0 1) "

あえて select: #isBinary としていることからもわかるように二項セレクター(セレクターはメソッド名のこと。二項~は通常の言語でいうところの二項演算子ですが、Smalltalk には演算子という概念が無くすべてメソッドなので、単に記号のみからなる1引数をとるセレクター…程度の分類)にこだわらなければ、同プロトコルには他言語での ** に相当する #raisedTo: も含まれます。

#(1 2 3) raisedTo: 4 "=> #(1 16 81) "

なお Pharo では #raisedTo: の代わりに #** も使えるのですが、本稿のテーマとしては肝心の Collection>>#** が定義されていないので、#(1 2 3) ** 4 はできません。あしからず。

math functions プロトコルに属するメソッド群との違い

二項演算の arithmetic プロトコルに対し、主に単項メッセージセレクターで構成される math functions プロトコルに属するメソッド群も配列の各要素に対して同名メッセージを送信して得られた結果の要素に持つ配列を返す動きをします。

#(1 2 3) negated "=> #(-1 -2 -3) "
#(1 -2 3) abs "=> #(1 2 3) "
#(1 -2 3) sign "=> #(1 -1 1) "
#(1 2 3) squared "=> #(1 4 9) "
#(1 2 3) sqrt "=> #(1 1.4142135623730951 1.7320508075688772) "

余談ですがこのプロトコルには、#max #min #middle や #sum、#average などといった、配列を返さない統計処理寄りのメソッドも含まれます。

#(1 2 3) max "=> 3 "
#(1 2 3) middle "=> 2 "
#(1 2 3) sum "=> 6 "
#(1 2 3) average "=> 2 "

先に紹介した配列の各要素に同じメッセージを送信して結果の配列を得るタイプのメソッドの実装はそのままで直感的でシンプルです。

Collection >> abs
    "Absolute value of all elements in the collection"
    ^ self collect: [:a | a abs]

ブロック(無名関数)を引数に取るメソッド呼び出しに際し、ブロック内での処理が要素への単項メッセージ送信のみならそのブロックの代わりにシンボルで代用するのが普通になった今なら、このようにも書けますね。

Collection >> abs
    ^ self collect: #abs

では、arithmetic プロトコルのメソッド群はどのように実装されているか。math functions と同様にまず思いつくのはこういう実装です。

Collection >> * arg
    ^ self collect: [:x | x * arg]

ところが実際の実装はこうです。

Collection >> * arg
    ^ arg adaptToCollection: self andSend: #*

引数である arg に対して改めてメッセージを送る「ダブルディスパッチ」機構が適用されています。

arithmetic プロトコルのメソッド群のちょっと変わった実装が意味するところ

では、#adaptToCollection:andSend: メソッドは何をしているのか調べてみましょう。クラスブラウザなどでコードペインの implementors ボタンをクリックするか(Squeak の場合)、セレクター一覧のペインの右クリックメニューから implementors of ... コマンドを選択するか、あるいはコードの adaptToCollection: self andSend: をドラッグして選択して(ちなみにこのとき、self などの引数はいちいち削除する必要はありません)Squeak なら alt/cmd + m、Pharo なら ctrl + m をタイプすると #adaptToCollection:andSend: を定義しているクラス(とその実装)を一覧できます。

Pharo-implementorsOfIt.png

Pharo には Complex や String に対する同メソッドの定義が見当たりませんが(Complex については、Pharo ではそもそも組み込みでは無いので当然…)、それでも Number の他、Fraction(分数)や Point(座標)、Collection といった複数のクラスに #adaptToCollection:andSend: が定義されているのがわかります。(図は Squeak の場合)

Squeak-implesOfAdaptToCollectionAndSend.png

まずは、Number をクリックして #(1 2 3) * 4 などとしたときにコールされるであろう Number >> #adaptToCollection:andSend: の定義を見てみましょう。

Number >> adaptToCollection: rcvr andSend: selector
    ^ rcvr collect: [:element | element perform: selector with: self]

ここで rcvr (おそらく「レシーバー」の意)はこのメソッドをコールした Collection >> #* でのレシーバーである配列(#(1 2 3) * 4 なら #(1 2 3) )、selector は #*、self は 4 なので、この式が実行するのはなんのことはない普通の次のような式です。

#(1 2 3) collect: [:element | element * 4]

たしかにこれなら #(4 8 12) が返るのがわかります。Fraction や Complex でもまったく同じ定義です。(とはいえ、なぜ 3 * (4/5) が機能するかは関連して調べてみる価値はありそうですが、今はあえてスルーします。^^; )

#(1 2 3) * (4/5) "=> {(4/5) . (8/5) . (12/5)} "
#(1 2 3) * (4@5) "=> {4@5 . 8@10 . 12@15} "

Pharo にはない String>>#adaptToCollection:andSend: では、String をいったん Number に変換する作業を経て再度呼び出しというかたちをとってはいますが、結果的に同じ処理をしています。

String >> adaptToCollection: rcvr andSend: selector
    ^ rcvr perform: selector with: self asNumber
#(1 2 3) * '4' "=> #(4 8 12) "

ではなぜわざわざダブルディスパッチにしているのでしょうか?

実は、arithmetic プロトコルに属するメソッドのダブルディスパッチ機構が活きるのは引数が Collection のとき。すなわち、次のような式が評価可能になるのです。

#(1 2 3) * #(4 5 6) "=> #(4 10 18) "
#(1 2 3) raisedTo: #(4 5 6) "=> #(1 32 729) "

Squeak/Pharo…、恐ろしい子!

余談ですが、実は Number >> #* など(より正確にはそれをオーバーライドしたサブクラスの #* など)にも同様の機構が組み込まれているので、こんな式も評価できたりします。(上でスルーしたやつと同じしくみです。よかったら調べてみてください。)

3 * #(4 5 6) "=> #(12 15 18) "

配列編算で記述がすっきり

あいにく Pharo には #stdev が組み込みですでにあるので(ただし n-1 )例としてはあまり有り難みがわかりにくいですが、たとえば標準偏差を求める式は次のように書くことができます。

| xs |
xs := #(96 63 85 66 91 89 77).
((xs - xs average raisedTo: 2) sum / xs size) sqrt "=> 11.77163661397295 "

効率面での善し悪しはさておき、ループでぐるぐる回すよりコードが直感的で簡潔になるのがいいですね。

参考:【統計学】初めての「標準偏差」(統計学に挫折しないために)

sumim
Smalltalkのファン。パーソナルコンピューティングの歴史や未来に興味を持つ。Smalltalkはアラン・ケイらが理想のPC向け暫定的OSとして70年代に試作し以降、GUIやIDE、OOPやデザパタ、開発手法等を創出、模倣されることで世に広めた。Smalltalkは、教養や古典である一方でアグレッシブな pharo.org などとしても今なお進化を続け、同時に他者へ影響を与え続けている。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした