APL で書かれたライフゲームを Squeak Smalltalk に翻訳 (ついでに Ruby版も)

未来の言語は「APL」? Rubyのまつもと氏が講演 − @IT で登場した APL で書かれたライフゲームが何をやっているのかさっぱりだったのが悔しかったので、とりあえずちょっと勉強してだいたいの内容が分かるくらいには読めるようにしてから Squeak Smalltalk でほぼ同じことをするコードに書き直してみました。

life ← {↑1 ω∨.^3 4=+/,¯1 0 1∘.Θ¯1 0 1∘.Φ⊂ω}

細かいことですが、くだんのスライドでは二番目の ¯ のあとの 1 が抜けていますね。^^;



life := ... のところのブロック定義が上の APL のコード(life ← ...)に相当します。もちろん APL には短さはもちろん流れの美しさの面でもとうていかなわないわけですが、標準ライブラリ・組み込みメソッドのみで似たような処理をこの程度で書ける言語もそう多くはないと思います。

| mat1 elems mat2 life |
elems := (1 to: 9) collect: [:each |
    (#(2 3 4 5 8) includes: each) ifTrue: [1] ifFalse: [0]].
mat1 := Matrix rows: 3 columns: 3 contents: elems.
mat2 := (Matrix zeros: 5) atRows: 2 to: 4 columns: 2 to: 4 put: mat1; yourself.

life := [:ω |
    | ωs sum survivers |
    ωs := OrderedCollection new.
    #(-1 0 1) asDigitsToPower: 2 do: [:xy |
        ωs add: (ω
            atRows: ((1 to: ω rowCount) flipRotated: xy first * 2)
            columns: ((1 to: ω columnCount) flipRotated: xy second * 2))].
    sum := ωs sum.
    survivers := #(3 4) collect: [:each |
        sum collect: [:elem | elem = each ifTrue: [1] ifFalse: [0]]].
    (survivers * {1. ω}) sum].

mat2.
 
"=> a Matrix(
0 0 0 0 0
0 0 1 1 0
0 1 1 0 0
0 0 1 0 0
0 0 0 0 0) "

life value: mat2
 
"=> a Matrix(
0 0 0 0 0
0 1 1 1 0
0 1 0 0 0
0 1 1 0 0
0 0 0 0 0) "


使用しているメソッドの簡単な解説を以下に。


#flipRotated: は (引数の絶対値 + 1) ÷ 2 の商の数だけ配列の要素を、引数が正の時は右方向、負の時は左方向にシフトして返します。

(1 to: 5) flipRotated: 2   "=> #(2 3 4 5 1) "
(1 to: 5) flipRotated: 4   "=> #(3 4 5 1 2) "


なお、引数が奇数のときは順序が逆になります。

(1 to: 5) flipRotated: 1   "=> #(4 3 2 1 5) "
(1 to: 5) flipRotated: 3   "=> #(3 2 1 5 4) "

素直にローテートしてくれるメソッドがなかったので使いましたがなんとも不思議な…。



#atRows:columns: は引数として与えた配列の要素に対応する番号の列または行を並び替えたマトリックスを返します。

| mat |
mat := Matrix diagonal: (1 to: 5).

"=> a Matrix(
1 0 0 0 0
0 2 0 0 0
0 0 3 0 0
0 0 0 4 0
0 0 0 0 5) "

mat atRows: (1 to: 5) columns: #(1 3 5 2 4)

"=> a Matrix(
1 0 0 0 0
0 0 0 2 0
0 3 0 0 0
0 0 0 0 4
0 0 5 0 0) "



Squeak Smalltalk の順序付きコレクションに対する四則演算などの振る舞いは他の Smalltalk 処理系とはちょっと変わっていて(おそらく初期の Smalltalk にあった APL ゆずりの機能を復活させたものかと…)、引数が数値ならレシーバの各要素に対する演算、引数が同じコレクションなら対応する要素同士の演算の結果をコレクションで返します。

#(1 2 3) * 2          "=> #(2 4 6) "
#(1 2 3) * #(4 5 6)   "=> #(4 10 18) "

いつもどおり、Smalltalk からさらに Ruby へ翻訳した版も書いてみようか…とも思ったのですが、マトリックスはともかく、最後の便利な配列の演算機能がないと最後のまとめの部分とかがキツそうなので今回は断念しました。…と思い込んでいたのですが、よくよく調べてみれば Ruby でも(配列はダメですが)Matrix でなら似た手が使えるようなので、そのうち書いてみます。


追記:
なんとか書いてはみましたが、けっこうひどいことになっています。もう少し Ruby力をつけないとダメですね。^^; (Ruby1.8 で動きます)

require 'enumerator'
require 'matrix'
class Matrix
  def []=(ri,ci,v); @rows[ri][ci]=v end
  def phi(n); r=@rows.dup; n.abs.times{ n>0 ? r<<r.shift : r.unshift(r.pop) }; self.class[*r] end
  def theta(n); t.phi(n).t end
  def mul(m); vs=@rows.flatten; i=-1; m.collect{ |e| vs[i+=1]*e } end
end

elems = (1..9).collect{ |e| ([2,3,4,5,8].include?(e)) ? 1 : 0 }
mat1 = Matrix[*elems.enum_slice(3).to_a]
mat2 = Matrix.zero(5)
mat1.row_size.times{ |ri| mat1.column_size.times{ |ci| mat2[ri+1,ci+1]=mat1[ri,ci] } }

def life(ome)
  omes =  [-1,0,1].collect{ |dx|
    [-1,0,1].collect{ |dy| ome.theta(dy) }.collect{ |m| m.phi(dx) }}.flatten
  sum = omes.inject{ |s,m| s + m }
  survivers = [3,4].collect{ |ea| sum.collect{ |el| el == ea ? 1 : 0 } }
  survivers[0] + survivers[1].mul(ome)
end

p mat2
#=> Matrix[
[0, 0, 0, 0, 0],
[0, 0, 1, 1, 0],
[0, 1, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]]

p life(mat2)
#=> Matrix[
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 0, 0, 0],
[0, 1, 1, 0, 0],
[0, 0, 0, 0, 0]]


追記:

Ruby版 を改訂してみました!(Ruby2.7preview1で動作を確認しています)

require 'matrix'

class Matrix
  def phi(n); r=@rows.dup; n.abs.times{ n>0 ? r<<r.shift : r.unshift(r.pop) }; self.class[*r] end
  def theta(n); t.phi(n).t end
end

elems = (1..9).collect{ |pos| ([3,4,6,8,9].include?(pos)) ? 1 : 0 }
(mat = Matrix.zero(20))[0..2, 0..2] = Matrix[*elems.each_slice(3).to_a]

def life(omega)
  omegas =  [-1,0,1].collect{ |dx| [-1,0,1].collect{ |dy| omega.theta(dy) }.collect{ |m| m.phi(dx) }}.flatten
  sum = omegas.inject(&:+)
  survivers = [3,4].collect{ |x| sum.collect{ |elem| elem == x ? 1 : 0 } }
  survivers[0] + survivers[1].entrywise_product(omega)
end

mat.to_a.each{ |r| p r }
100.times{ print "\n"; sleep 0.1; (mat = life(mat)).to_a.each{ |r| p r } }


参考: