あみだくじジェネレータを状態をなるべく扱わない方向で書く

みなとRuby会議01というイベントで、ペアプロ大会みたいなのがあったらしい。

http://willnet.in/10

あみだくじ。

いろんな人の書いた結果が残ってるらしいし、勉強になるかなと思って、ぼくもやってみた。

どう書くのかなと思って、まず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っぽく完成した形で書く人までいる。