Labeled break, next, and redo in Ruby
Many languages support breaking out of nested loops. There are some typical ways of doing this:
- Some languages can name loops by providing a label for the loop. In those languages, you can use
breaktogether with a label to specify which loop to break out of. Examples: Perl, Java, JavaScript, and some others. - Some languages can specify the number of layers of loops to break out of. In those languages, you can use
breaktogether with a number to specify how many layers of loops to break out of. The only example that I know is C#. - Some languages have
gotostatements. You can easily break from loops to wherever you want by usinggoto(actually breaking out of nested loops is among the only recommended cases for usinggoto). Examples: C, C++.
However, in most other languages, it is not easy to break out of nested loops. A typical solution is this:
1 2 3 4 5 6 7 8 9 10 |
outer_loop do
break_outer = false
inner_loop do
if condition
break_outer = true
break
end
end
break if break_outer
end
|
In languages with exceptions, another possible workaround is to use exceptions (the catch–throw control flow):
1 2 3 4 5 6 7 |
catch :outer_loop do
outer_loop do
inner_loop do
throw :outer_loop if condition
end
end
end
|
I wrote a simple module to better use this workaround.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class JumpLabel < StandardError
attr_reader :reason, :arg
{break: true, next: true, redo: false}.each do |reason, has_args|
define_method reason do |*args|
@reason = reason
@arg = args.size == 1 ? args.first : args if has_args
raise self
end
end
end
class Module
def register_label *method_names
method_names.each do |name|
old = instance_method name
define_method name do |*args, **opts, &block|
return old.bind_call self, *args, **opts unless block
old.bind_call self, *args, **opts do |*bargs, **bopts, &bblock|
block.call *bargs, **bopts, jump_label: label = JumpLabel.new, &bblock
rescue JumpLabel => catched_label
raise catched_label unless catched_label == label
case label.reason
when :break then break label.arg
when :next then next label.arg
when :redo then redo
end
end
end
end
end
end
|
Example usage:
1 2 3 4 5 6 7 8 9 |
Integer.register_label :upto, :downto
1.upto 520 do |i, jump_label:|
print i
1.downto -1314 do |j|
print j
jump_label.break 8 if j == 0
end
end.tap { puts _1 }
# => 1108
|
