4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SmalltalkAdvent Calendar 2016

Day 4

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

Last updated at Posted at 2016-12-04

#はじめに

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 "

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

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?