11

As every Ruby programmer eventually discovers, calling blocks or procs that contain return statements can be dangerous as this might exit your current context:

def some_method(&_block)
   puts 1
   yield
   # The following line will never be executed in this example
   # as the yield is actually a `yield-and-return`.
   puts 3
end

def test
  some_method do
    puts 2
    return
  end
end

test

# This prints "1\n2\n" instead of "1\n2\n3\n"    

In cases you want to be absolutely sure some of your code runs after you called a block or proc, you can use a begin ... ensure construct. But since ensure is also called if there is an exception during yield, it requires a little more work.

I've created a tiny module that deals with this problem in two different ways:

  1. Using safe_yield, it is detected whether the yielded block or proc actually returns using the return keyword. If so, it raises an exception.

    unknown_block = proc do
      return
    end 
    
    ReturnSafeYield.safe_yield(unknown_block)
    # => Raises a UnexpectedReturnException exception
    
  2. Using call_then_yield, you can call a block and then make sure that a second block is executed, even if the first block contains a return statement.

    unknown_block = proc do
      return
    end
    ReturnSafeYield.call_then_yield(unknown_block) do
      # => This line is called even though the above block contains a `return`.
    end
    

I'm considering to create a quick Gem out of this, or is there any built-in solution to prevent quick return from the nested block which I missed?

23
  • 7
    This will break a default ruby behavior, bringing more pain, than profit. Imagine I am the consumer of the code that uses this trick. As I put return inside my block, I expect it to pass control immediately, and I would be damn surprised that some weird exception was raised. Commented Dec 12, 2016 at 12:43
  • 5
    Covering hunting pits with a hay only hides a trap, making code harder to debug. Ruby is not a language to protect people from shooting their legs, and this is the main advantage of it. Commented Dec 12, 2016 at 12:47
  • 3
    This is not a good idea, but it is a good question. Thank you for asking an interesting question. Commented Dec 12, 2016 at 12:49
  • 2
    Why would you pass (or even create) a proc containing a return statement in the first place? Commented Dec 12, 2016 at 12:51
  • 5
    Me, I believe that users should be totally allowed to shoot themselves in the foot. Commented Dec 12, 2016 at 14:19

1 Answer 1

3

There is a built-in solution to detect whether a block contains a return statement.

You can use RubyVM::InstructionSequence.disasm to disassemble the block passed in by the user, then search it for throw 1, which represents a return statement.

Here's a sample implementation:

def safe_yield(&block)
  if RubyVM::InstructionSequence.disasm(block) =~ /^\d+ throw +1$/
    raise LocalJumpError
  end

  block.call
end

Here's how you might incorporate it into your library:

def library_method(&block)
  safe_yield(&block)
  puts "library_method succeeded"
rescue LocalJumpError
  puts "library_method encountered illegal return but resumed execution"
end

And here's the user experience for a well-behaved and a misbehaving user:

def nice_user_method
  library_method { 1 + 1 }
end

nice_user_method
# library_method succeeded

def naughty_user_method
  library_method { return false if rand > 0.5 }
end

naughty_user_method
# library_method encountered illegal return but resumed execution

Commentary:

Using raise LocalJumpError/rescue LocalJumpError gets around the issues you encountered when using a blanket ensure.

I chose LocalJumpError because it seems relevant, and because (I think!) there is no possible Ruby code that would result in LocalJumpError being raised "naturally" in this context. If that turns out to be false, you can easily substitute your own new exception class.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.