Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding returning from procs in Ruby

I was wondering how to pass a block to a method which will make the method return on yield.

The naive aproach doesn't work:

def run(&block)
  block.call
end

run { return :foo } # => LocalJumpError

Wrapping in another proc has the same effect:

def run(&block)
  proc { block.call }.call
end

run { return :bar } # => LocalJumpError

So I thought that the return statement is bound to the receiver of the current binding. However, trying it out with instance_eval proved me wrong:

class ProcTest
  def run(&block)
    puts "run: #{[binding.local_variables, binding.receiver]}"
    instance_eval(&block)
  end
end

pt = ProcTest.new
binding_inspector = proc { puts "proc: #{[binding.local_variables, binding.receiver]}" }
puts "main: #{[binding.local_variables, binding.receiver]}"
    # => main: [[:pt, :binding_inspector], main]
binding_inspector.call
    # => proc: [[:pt, :binding_inspector], main]
pt.run(&binding_inspector)
    # => run: [[:block], #<ProcTest:0x007f4987b06508>]
    # => proc: [[:pt, :binding_inspector], #<ProcTest:0x007f4987b06508>]
pt.run { return :baz }
    # => run: [[:block], #<ProcTest:0x007f4987b06508>]
    # => LocalJumpError

So the questions are:

  1. How can this be done?
  2. How is the return context tied to the return statement. Is this connection accessible via the language's API?
  3. Was this implemented in such manner intentionally? If yes - why? If no - what are the obstacles to fix it?
like image 260
ndnenkov Avatar asked Oct 19 '22 04:10

ndnenkov


1 Answers

I thought that the return statement is bound to the receiver of the current binding.

Only methods have an receiver. return is not a method:

defined? return #=> "expression"

Trying to invoke it as a method doesn't work:

def foo
  send(:return, 123)
end

foo #=> undefined method `return'

trying it out with instance_eval proved me wrong

Though instance_eval evaluates the block in the context of the receiver (so you have access to the receivers instance methods and instance variables):

class MyClass
   def foo(&block)
     @var = 123
     instance_eval(&block)
   end
end

MyClass.new.foo { instance_variables }
#=> [:@var]

... it does not evaluate the block in the current binding (so you don't have access to any local variables):

class MyClass
   def foo(&block)
     var = 123
     instance_eval(&block)
   end
end

MyClass.new.foo { local_variables }
#=> []

How can this be done?

You could use eval, but that requires a string:

def foo
  var = 123
  eval yield
  nil
end

foo { "return var * 2" }
#=> 246

Or by passing the binding to the block (again using eval):

def foo
  var = 123
  yield binding
  nil
end

foo { |b| b.eval "return var * 2" }
#=> 246
like image 90
Stefan Avatar answered Nov 04 '22 20:11

Stefan