Using 'return' in a Ruby block

▼魔方 西西 提交于 2019-11-26 12:21:43

问题


I\'m trying to use Ruby 1.9.1 for an embedded scripting language, so that \"end-user\" code gets written in a Ruby block. One issue with this is that I\'d like the users to be able to use the \'return\' keyword in the blocks, so they don\'t need to worry about implicit return values. With this in mind, this is the kind of thing I\'d like to be able to do:

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

thing {
  return 6 * 7
}

If I use \'return\' in the above example, I get a LocalJumpError. I\'m aware that this is because the block in question is a Proc and not a lambda. The code works if I remove \'return\', but I\'d really prefer to be able to use \'return\' in this scenario. Is this possible? I\'ve tried converting the block to a lambda, but the result is the same.


回答1:


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



回答2:


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
# ...



回答3:


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
}



回答4:


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



回答5:


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).




回答6:


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.




回答7:


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 }


来源:https://stackoverflow.com/questions/2325471/using-return-in-a-ruby-block

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!