在 Ruby 块中使用‘ return’

我正在尝试将 Ruby 1.9.1用于嵌入式脚本语言,这样“最终用户”的代码就可以用 Ruby 块编写。这样做的一个问题是,我希望用户能够在块中使用“ return”关键字,这样他们就不需要担心隐式返回值。考虑到这一点,这就是我希望能够做到的事情:

def thing(*args, &block)
value = block.call
puts "value=#{value}"
end


thing {
return 6 * 7
}

如果我在上面的例子中使用‘ return’,我会得到一个 LocalJumpError。我知道这是因为这个块是 Proc 而不是 Lambda。如果删除“ return”,代码就可以工作,但是我更希望在这个场景中能够使用“ return”。这可能吗?我试过将这个块转换为 lambda,但结果是一样的。

71666 次浏览

Where is thing invoked? Are you inside a class?

You may consider using something like this:

class MyThing
def ret b
@retval = b
end


def thing(*args, &block)
implicit = block.call
value = @retval || implicit
puts "value=#{value}"
end


def example1
thing do
ret 5 * 6
4
end
end


def example2
thing do
5 * 6
end
end
end

You are looking it from the wrong point of view. This is an issue of thing, not the lambda.

def thing(*args, &block)
block.call.tap do |value|
puts "value=#{value}"
end
end


thing {
6 * 7
}

You cannot do that in Ruby.

The return keyword always returns from the method or lambda in the current context. In blocks, it will return from the method in which the closure was defined. It cannot be made to return from the calling method or lambda.

The Rubyspec demonstrates that this is indeed the correct behaviour for Ruby (admittedly not a real implementation, but aims full compatibility with C Ruby):

describe "The return keyword" do
# ...
describe "within a block" do
# ...
it "causes the method that lexically encloses the block to return" do
# ...
it "returns from the lexically enclosing method even in case of chained calls" do
# ...

Simply use next in this context:

$ irb
irb(main):001:0> def thing(*args, &block)
irb(main):002:1>   value = block.call
irb(main):003:1>   puts "value=#{value}"
irb(main):004:1> end
=> nil
irb(main):005:0>
irb(main):006:0* thing {
irb(main):007:1*   return 6 * 7
irb(main):008:1> }
LocalJumpError: unexpected return
from (irb):7:in `block in irb_binding'
from (irb):2:in `call'
from (irb):2:in `thing'
from (irb):6
from /home/mirko/.rvm/rubies/ruby-1.9.1-p378/bin/irb:15:in `<main>'
irb(main):009:0> thing { break 6 * 7 }
=> 42
irb(main):011:0> thing { next 6 * 7 }
value=42
=> nil
  • return always returns from method, but if you test this snippet in irb you don't have method, that's why you have LocalJumpError
  • break returns value from block and ends its call. If your block was called by yield or .call, then break breaks from this iterator too
  • next returns value from block and ends its call. If your block was called by yield or .call, then next returns value to line where yield was called

I had the same issue writing a DSL for a web framework in ruby... (the web framework Anorexic will rock!)...

anyway, I dug into the ruby internals and found a simple solution using the LocalJumpError returned when a Proc calls return... it runs well in the tests so far, but I'm not sure it's full-proof:

def thing(*args, &block)
if block
block_response = nil
begin
block_response = block.call
rescue Exception => e
if e.message == "unexpected return"
block_response = e.exit_value
else
raise e
end
end
puts "value=#{block_response}"
else
puts "no block given"
end
end

the if statement in the rescue segment could probably look something like this:

if e.is_a? LocalJumpError

but it's uncharted territory for me, so I'll stick to what I tested so far.

I believe this is the correct answer, despite the drawbacks:

def return_wrap(&block)
Thread.new { return yield }.join
rescue LocalJumpError => ex
ex.exit_value
end


def thing(*args, &block)
value = return_wrap(&block)
puts "value=#{value}"
end


thing {
return 6 * 7
}

This hack allows users to use return in their procs without consequences, self is preserved, etc.

The advantage of using Thread here is that in some cases you won't get the LocalJumpError - and the return will happen in the most unexpected place (onside a top-level method, unexpectedly skipping the rest of it's body).

The main disadvantage is the potential overhead (you can replace the Thread+join with just the yield if that's enough in your scenario).

I found a way, but it involves defining a method as an intermediate step:

def thing(*args, &block)
define_method(:__thing, &block)
puts "value=#{__thing}"
end


thing { return 6 * 7 }

I admire the answer of s12chung. Here is my little improvement of his answer. It lets avoid cluttering the context with method __thing.

def thing(*args, &block)
o = Object.new
o.define_singleton_method(:__thing, block)
puts "value=#{o.__thing}"
end


thing { return 6 * 7 }