「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#product
やcatch
/throw
を使わず、find
とbreak
を使って書いてみたのが以下。
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の実装を見てみると、ブロックが渡されているときには組み合わせを一つ生成するごとにブロックを呼んでいるようだから、find
とbreak
を使ったバージョンと同じ程度の効率で動くことが期待できる。
また、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#combination
やArray#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のことらしい。
関連
- つくってみた - I like Ruby too.:
Array#product
のEnumerable版、Enumerable#product_enum
の実装。rewindを持つEnumeratorを引数に取ってEnumeratorを返す。