Squeak Smalltalk で 配列を交互に分配


Squeak Smalltalk でならどう書くかなぁ…と、いろいろ考えてみたのですが、いまひとつしっくりくるものがないので、いくつか思いついた書き方をざっくばらんに晒しておきます。


お題は、#(x0 y0 x1 y1 x2 y2) を #( (x0 x1 x2) (y0 y1 y2) ) のように分配してまとめるというもの(元はリストなのですが Smalltalk なので配列に読み替えます)。


オーソドックスに #( (x0 y0) (x1 y1) (x2 y2) ) という中間生成物を介して作る方法だとこう書けます。

(#(x0 y0 x1 y1 x2 y2) groupsOf: 2 atATimeCollect: [:pair | pair])
   inject: #(() ()) into: [:sum :each |
      sum with: each collect: [:a :b | a, {b}]]  "=> #((x0 x1 x2) (y0 y1 y2)) "


こういうときは、入れ子の配列を二次元配列に見立てて、行と列を入れ替える #transpose とか #transposed みたいな節操のないメソッドが欲しくなります。

SequenceableCollection >> transposed
   | colns |
   colns := (self species new: self first size) collect: [:idx | self first species new].
   self do: [:each | colns := colns with: each collect: [:coln :elem | coln copyWith: elem]].
   ^colns
#('abc' 'def') transposed  "=> #('ad' 'be' 'cf') "
(#(x0 y0 x1 y1 x2 y2) groupsOf: 2 atATimeCollect: [:xs | xs]) transposed  "=> #((x0 x1 x2) (y0 y1 y2)) "


余談ですが、ここで引数を返すだけのブロック(無名関数)である [:xs | xs] をシンボル #yourself に置き換えられないのは、メソッド #groupsOf:atATimeCollect: が第二引数(通常はブロック)に対して numArgs をして 1 かそれ以外で条件分岐をしているというありがた迷惑な実装のためです。

SequenceableCollection  >> groupsOf: n atATimeCollect: aBlock 
   | passArray |
   passArray := aBlock numArgs = 1.
   ^(n to: self size by: n)
      collect: [:index | 
         | args |
         args := (self copyFrom: index - n + 1 to: index) asArray.
         passArray
            ifTrue: [aBlock value: args]
            ifFalse: [aBlock valueWithArguments: args]]


このため、ブロックはグループを配列としてまるごとを受け取る処理も、その要素を個別にブロック引数に受け取る処理としても書けるようになっていて便利ではあるのですが、あいにくブロックの代わりにシンボルを渡すと破綻します(なお、エラーにならないことからわかるようにシンボルも numArgs への応答はできているので、破綻しないように細工することは簡単です)。個人的にはこういう一見便利そうなカラクリにはなるほどっ!と思うよりはイラッ!とさせられることのほうが多いです。


ありがた迷惑といえば、最近の String>>#, の実装もそうで、引数を asString するようになってしまったため、文字列以外の物を文字列として結合するのには便利なのですが、

'abc', 123 "=> 'abc123' "


しかし、バイト列に見立てた配列を連結するとか以前の実装に依存したちょっと変態的なコードを書こうとしたときにそれが妨げられて脱力します。まあ難しいところです。

'abc', #(65 66 67)  "=> 'abc#(65 66 67)'。'abcABC' を期待 "


閑話休題


Smalltalk の配列は要素の追加ができませんが、OrderedCollection や WriteStream のインスタンスを使えば破壊的に順次追加する処理を書け、さらに #atWrap: を使ってひとひねりできます。

| colns |
colns := #(() ()) collect: #asOrderedCollection.
#(x0 y0 x1 y1 x2 y2) doWithIndex: [:each :idx | (colns atWrap: idx) add: each].
^colns collect: #asArray  "=> #((x0 x1 x2) (y0 y1 y2)) "
| strms |
strms := #(() ()) collect: #writeStream.
#(x0 y0 x1 y1 x2 y2) doWithIndex: [:each :idx | (strms atWrap: idx) nextPut: each].
^strms collect: #contents  "=> #((x0 x1 x2) (y0 y1 y2)) "


もうひとひねりして、追加先のコレクションやストリームをローテートする書き方もできますが、これはちょっとやり過ぎでしょうね。^^;

| colns |
colns := #(() ()) asOrderedCollection collect: #asOrderedCollection.
#(x0 y0 x1 y1 x2 y2) do: [:each | (colns add: colns removeFirst) add: each].
^colns asArray collect: #asArray  "=> #((x0 x1 x2) (y0 y1 y2)) "


と、いいつつさらに悪のり。

| strm colns |
strm := #(x0 y0 x1 y1 x2 y2) readStream.
colns := #(() ()) asOrderedCollection collect: #asOrderedCollection.
[strm atEnd] whileFalse: [(colns add: colns removeFirst) add: strm next].
^colns asArray collect: #asArray  "=> #((x0 x1 x2) (y0 y1 y2)) "


目先を変えて Matrix のインスタンスを使う手もあります。が、Array2D の代替として導入されたわりに、いまいち頼りにならない Matrix のことなので、これを使って書ける処理もそれなりです。

| mat |
mat := Matrix rows: 3 columns: 2 contents: #(x0 y0 x1 y1 x2 y2).
^(1 to: mat columnCount) collect: [:idx | mat atColumn: idx]   "=> #((x0 x1 x2) (y0 y1 y2)) "


総じて、スマートに書くことができず残念な結果に終わりましたが、いろいろと考えたり考えさせられたりして楽しかったのでよしとしましょう。