Using 'return' in a Ruby block
Asked Answered
E

8

109

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.

Encroachment answered 24/2, 2010 at 11:15 Comment(2)
why do you want to avoid an implicit return value?Stoichiometric
@Stoichiometric - I have a related question here - #25954019 .Frodin
M
201

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
Microorganism answered 24/2, 2010 at 11:55 Comment(6)
break in a proc will raise an exceptionGarratt
can you cite where you get this information from that "next returns value from block and ends it call". I want to read more on it.Sublapsarianism
It was from The Ruby Programming Language book (I don't have it at hand right now) if I remember correctly. I just checked google and I believe it's from that book: librairie.immateriel.fr/fr/read_book/9780596516178/… and 2 next pagex from there (it's not my content and my pages, I just googled it). But I really recommend original book, it has much more gems explained.Microorganism
Also I answered from my head, only checking things in irb, that's why my answer is not technical or complete. For more informations check The Ruby Programming Language book.Microorganism
I wish this answer was at the top. I can not upvote it enough.Gawk
Thank you! So, if searching for the most semantically similar option to return, it looks like it's next.Symposium
M
28

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
# ...
Melan answered 24/2, 2010 at 11:42 Comment(1)
There's a detailed article on returning from a block/proc hereSavagism
O
4

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 }
Ottie answered 5/5, 2021 at 5:2 Comment(0)
P
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
}
Pachydermatous answered 24/2, 2010 at 11:33 Comment(0)
S
2

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.

Statolatry answered 11/10, 2014 at 20:55 Comment(0)
L
2

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 }
Lanam answered 27/7, 2015 at 14:29 Comment(0)
P
1

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
Perren answered 24/2, 2010 at 11:32 Comment(0)
B
1

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

Barge answered 27/5, 2015 at 15:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.