Reputation: 52357
Let say there are three methods:
def foo
puts :start
x = 1
bar(x) # I want the method to terminate here
baz
end
def bar(x)
if x < 2
puts :finish_here
return
else
puts :continue
end
end
def baz
puts :finish
end
foo
#=> start
#=> finish_here
#=> finish
#=> nil
Is there any (sane) way I could make the call to bar(x)
to be treated as if its code was executed in the scope of foo
, so that the return
terminates the foo
?
So the expected output of foo would be:
foo
#=> start
#=> finish_here
#=> nil
Using a Proc object instead of method or raising an exception or exit
ing the whole program are not the options.
Upvotes: 3
Views: 99
Reputation: 84132
As far as I know, you can't do this in pure ruby without some cooperation of the calling method, as outlined in the other answers.
If you are willing to restrict yourself to MRI and to use additional libraries, you could use the binding_of_caller gem (extracted from pry): this uses a C extension to allows you to get the binding object for the calling method (you can go as far up the call tree as you want).
require 'binding_of_caller'
def foo
puts :start
x = 1
bar(x) # I want the method to terminate here
baz
end
def bar(x)
if x < 2
puts :finish_here
binding.of_caller(1).eval("return")
else
puts :continue
end
end
def baz
puts :finish
end
foo
This does produce the output you want, but as has been commented this is rather unusual and likely to cause surprises. The author of the gem also warns against using in production
Upvotes: 1
Reputation: 19040
You can simulate that using throw
/catch
(which is not rescue
/raise
):
def foo
puts :start
x = 1
catch(:return) do
bar(x) # I want the method to terminate here
baz
end
end
def bar(x)
if x < 2
puts :finish_here
throw :return
else
puts :continue
end
end
def baz
puts :finish
end
foo
This is not using raise
/rescue
.
The catch
block is like an exception handler... But throw
/catch
in Ruby is usually used to handle non-exceptional situations, e.g., break out of inner loops, etc.
The throw
will traverse its callstack to try and find an associated catch
block with the same label (if you catch(:foo)
but throw :bar
, that's not going to work).
This is easier than returning a value to indicate termination, if you have a lot of nested calls, and any of them might want to return.
Upvotes: 6
Reputation: 15954
Following Stefan's "proposal" in the comments, this seems to work:
alias :original_foo :foo
def foo
$last_binding = binding
original_foo
end
def bar(x)
if x < 2
puts :finish_here
eval("return", $last_binding)
else
puts :continue
end
end
If you can modify foo
(and bar
), I'd rather signal termination request from bar
to foo
through the return value. Like so:
def foo
puts :start
x = 1
return if bar(x) == :out # I want the method to terminate here
baz
end
def bar(x)
if x < 2
puts :finish_here
return :out
else
puts :continue
end
end
Upvotes: 1
Reputation: 10898
Rather than returning from bar
you could call exit
def bar(x)
puts :finish_here
exit if x > 2
end
Update:
If you need to continue execution in your original method, you could use exceptions to do this:
I would define your own exception class for this purpose
class MyCustomError < StandardError; end
def bar(x)
puts :finish_here
raise MyCustomError if x > 2
end
Then you can rescue from this error to finish whatever processing you need to:
def foo
puts :start
x = 1
bar(x) # I want the method to terminate here
baz
rescue MyCustomError
# do your remaining things here
end
Upvotes: 0