Ruby で Python の for else みたいなの

繰り返しを実行して break(あるいは return)されなかったら何かを実行するって時々欲しいのですけれど、Ruby では Python の for else みたいな構文がないのですよね。なので throw ~ catch を使って大域脱出したりするのだけれど、どうにかできないかと思って考えてみました。

Enumerator#with_else(f) {..} です。f は Proc オブジェクトです。each などの Enumerator をレシーバーとして、ブロック内で break が実行されなければ、繰り返しを終えたあとに f.call されます。ブロックがなければ Enumerator を返します。
こんな感じ。

f = proc do
  puts "break せずに実行されました!"
end

ar = []
5.times {ar << rand(5)}
ar.each.with_else(f) do |i|
  puts i
  break if i == 4
end

を実行すると

1
0
0
1
3
break せずに実行されました!

みたいな。break されると

0
4

という風に f は実行されません。

また、each_with_index などと組み合わせて

ar = []
5.times {ar << rand(5)}
ar.each_with_index.with_else(f) do |i, j|
  p [i, j]
  break if i == 4
end

で、

[0, 0]
[0, 1]
[1, 2]
[1, 3]
[0, 4]
break せずに実行されました!

なんてのも可能です。


コードはこんな感じ。
with_else.rb

class Enumerator
  def with_else(f, *args)
    if block_given?
      each {|*i| yield(*i)}
      f.call(*args)
    else
      Enumerator.new do |y|
        each do |*i|
          a = (i.size <= 1) ? i[0] : i
          y << a
        end
        f.call(*args)
      end
    end
  end
end

else 節の可変長引数の処理にちょっと悩みました。
 
じつは前にも考えたのだけれど、上のように落ち着きました。

でも、やっぱり throw ~ catch の方が見やすいですかね…。
 

Gem 化

調子にのって Gem にしました。まったく大したものではないですが…。(9/11)
with_else | RubyGems.org | your community gem host