Breaking nested loop, like each_slice with style
tl;dr
Use catch/throw
block:
catch :take_me_out do
100.downto(0).each_slice(10) do |j|
puts "Outer loop"
j.each do |i|
puts "Inner loop #{i}"
throw :take_me_out if i < 50
end
end
end
A deeper look
Every ruby developer in his life will face at least once this problem:
100.downto(0).each_slice(10) do |j|
puts "Outer loop"
j.each do |i|
puts "Inner loop #{i}"
break if i < 50
end
end
The naïve programmer will think he's done, runs the code and find a surprise:
Outer loop
Inner loop 100
Inner loop 99
...
Inner loop 52
Inner loop 51
Outer loop
Inner loop 50
Inner loop 49
Outer loop
Inner loop 40
Outer loop
Inner loop 30
Outer loop
Inner loop 20
Outer loop
Inner loop 10
Outer loop
That's obvious at a second look, the break
statement only breaks the inner loop.
So how do we deal with this problem?
A lot of solution in the web make use of begin rescue Exception
, but this can be a bad idea if you don't know what you are doing (never rescue Exception
). You may find your code work in a strange way.
A better way to handle this is to use the less-known throw catch
block:
catch :take_me_out do
100.downto(0).each_slice(10) do |j|
puts "Outer loop"
j.each do |i|
puts "Inner loop #{i}"
throw :take_me_out if i < 50
end
end
end
This will give us the expected result, letting us having rescue
block inside or outside the catch
block.
In this case the exception is handled inside the catch
, so our code will also work for 69..50
iterations.
catch :take_me_out do
100.downto(0).each_slice(10) do |j|
puts "Outer loop"
j.each do |i|
begin
puts "Inner loop #{i}"
i / (i - 70)
rescue ZeroDivisionError
puts "woops"
end
throw :take_me_out if i < 50
end
end
end
Here, instead, the rescue
wraps our catch
so execution will stop when we hit 70
.
begin
catch :take_me_out do
100.downto(0).each_slice(10) do |j|
puts "Outer loop"
j.each do |i|
puts "Inner loop #{i}"
i / (i - 70)
throw :take_me_out if i < 50
end
end
end
rescue ZeroDivisionError
puts "woops"
end