Andrey Deineko
Andrey Deineko

Reputation: 52357

How to force method termination to affect the one it is called in

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

EDIT

Using a Proc object instead of method or raising an exception or exiting the whole program are not the options.

Upvotes: 3

Views: 99

Answers (4)

Frederick Cheung
Frederick Cheung

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

Ven
Ven

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.

See it live on Coliru.

Upvotes: 6

undur_gongor
undur_gongor

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

Jon
Jon

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

Related Questions