「Effective Rubyのcatch/throwをproduct/findで書き換える」の感想

Effective Rubyのcatch/throwをproduct/findで書き換える - Qiitaを読んだ。

match = catch(:jump) do
  @characters.each do |character|
    @colors.each do |color|
      if player.valid?(character, color)
        throw(:jump, [character, color])
      end
    end
  end
end

というコードは

match = @characters.product(@colors).find {|params| player.valid?(*params) }

とリファクタリングできるという話。

配列が大きいようだと、product で大量の Array が作られることになるので throw-catch のコードのほうが効率的コメントで指摘されている。

Array#productcatch/throwを使わず、findbreakを使って書いてみたのが以下。

match = @characters.find {|character|
    valid_color = @colors.find {|color|
        player.valid?(character, color)
    }
    break [character, valid_color] if valid_color
}

Array#product版に比べるとメモリの消費量は少ないはずだが、これだとcatch/throwを使ったバージョンのほうが分かりやすい。

Array#productにブロックを渡す

Array#productにブロックを渡すと、組み合わせを一つごとにブロックの引数にして呼び出してくれるので、次のようにも書ける。

match = @characters.product(@colors){|params|
    break params if player.valid?(*params)
}

# `break`が実行されなかったときには`match == @characters`になるので、それをチェックする必要がある。
match = nil if match == @characters

CRubyの実装を見てみると、ブロックが渡されているときには組み合わせを一つ生成するごとにブロックを呼んでいるようだから、findbreakを使ったバージョンと同じ程度の効率で動くことが期待できる。

また、Object#to_enumを使うと

match = @characters.to_enum(:product, @colors).find {|params| player.valid?(*params) }

と書ける。見た目はproduct/find版に近い。

ただし、Rubiniusの実装ではArray#productはブロックの有無に関わらず、結果を一括して生成してしまう。やはりcatch/throwを使ったほうが安全だ。

Array#productはなぜEnumeratorを返さないのか

Array#combinationArray#permutationなどはブロックを渡さずに呼び出すとEnumeratorを返す。なぜArray#productだけがArrayを返すのかよく分からない。

Enumerator#zipもEnumeratorではなくArrayを返す。self以外の要素が関わっているのが問題なんだろうか。

RubyのChangelogをgrepしてみたがよく分からない。とりあえず、rb_ary_(combination|product|permutation)が追加されたのはSat Sep 29 17:31:04 2007のことらしい。

関連