Using 'return' in a Ruby block

后端 未结 7 787
太阳男子
太阳男子 2020-11-28 03:54

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

相关标签:
7条回答
  • 2020-11-28 04:08

    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
    0 讨论(0)
  • 2020-11-28 04:14

    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 }
    
    0 讨论(0)
  • 2020-11-28 04:15

    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
    
    0 讨论(0)
  • 2020-11-28 04:17

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

    0 讨论(0)
  • 2020-11-28 04:26

    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
    # ...
    
    0 讨论(0)
  • 2020-11-28 04:26

    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.

    0 讨论(0)
提交回复
热议问题