あみだくじジェネレータを状態をなるべく扱わない方向で書く
みなとRuby会議01というイベントで、ペアプロ大会みたいなのがあったらしい。
あみだくじ。
いろんな人の書いた結果が残ってるらしいし、勉強になるかなと思って、ぼくもやってみた。
どう書くのかなと思って、まず2、3行ほど何かを書いてみた。すぐに「同じ高さで隣り合う場所に横線があっちゃダメ」という当たり前のようで当たり前じゃない仕様に気付いた。あみだくじってそうだよな。手書きだと分かりづらいけど。
で、ふつうに手続き的な発想でいうと、2重ループにして、水平方向に結果を作っていくときに直前の状態を考慮して横線を引くかどうかを決めるということになるんだろうけど、もしかして、関数の適用だけでやったほうが書きやすくて読みやすくなるんじゃないのと思った。
1. 半分の確率で0か1を返す 2. 隣り合う1の一方を0に
という2つの関数があれば、0と1の並びに関数を適用していくだけでできそう。1が奇数回繰り返している場合は、2の操作を2度適用すればオッケー。で、書いてみたのが以下。
class Amida def initialize(size, height = 15) @height, @size = height, size @rows = [] height.times { @rows << Row.new(size - 1) } prepare_header_footer end def prepare_header_footer @header = ("A".."Z").take(@size).join(" ") arr = Array.new(@size, " ") arr[0] = "o " @footer = arr.shuffle.join end def show puts @header @rows.each do |r| puts "|" + r.with_bars + "|" end puts @footer end class Row < Array def initialize(col) super(col) { rand(2) } remove_continuous_ones end def remove_continuous_ones double_one_to_single_one double_one_to_single_one end def double_one_to_single_one self[0..-1] = join.gsub(/11/, '01').split(//).map(&:to_i) end def with_bars map {|i| i == 0 ? " " : "--" }.join('|') end end end if __FILE__ == $0 col = (ARGV.shift || 8).to_i a = Amida.new(col) a.show end
出力は以下のような感じ。
状態を持つ長いループがない分、スッキリしているように思う。
結局のところ、
join.gsub(/11/, '01').split(//).map(&:to_i)
という正規表現による力技の部分が状態を扱っているわけで、逐次的にやるのと本質的には変わらない。でも、DSLの力で読みやすいように思う。
書き終わってから、他の人の書いたあみだくじの実装を見て、バリエーションの多さにちょっと驚いた。いちばん違うなと思ったのは、どこまでRubyっぽいか、Railsっぽいかというところだったりする。moduleやclassを使わず、しかも頭から上に処理して結果が出るというようにミニマルなスクリプト的な書き方から、RSpecまで書きながら妙にGemっぽく完成した形で書く人までいる。