chifung7
chifung7

Reputation: 2621

Ruby return in yield block called from a method with ensure

def foo
  puts "in foo"
  s = yield
  puts "s = #{s}"
  return 2
ensure
  puts "in ensure"
  return 1
end

def bar
  foo do
    puts "in bar block"
    return 3
  end
  return 4
end


[36] pry(main)> r = bar
in foo
in bar block
in ensure
=> 4

I'd expect r = 3 but it turns out it is r = 4. If I remove the ensure code, r = 3 is expected. Why is it?

def foo
  puts "in foo"
  s = yield
  puts "s = #{s}"
  return 2
end

r = bar
in foo
in bar block
=> 3

Upvotes: 5

Views: 1857

Answers (1)

Oleg K.
Oleg K.

Reputation: 1549

It's a Ruby's feature of "unwinding the stack" from blocks. How your return works step by step:

  1. You return 3 from bar block. return_value = 3 and Ruby marks that it is a return value from block, so it should unwind the stack and return 3 from parent function. It would not return to foo at all if there was no ensure section. It is very important difference between returning from functions and from blocks.
  2. But Ruby always executes ensure, and there is one more return in ensure section of foo.
  3. After return 1 in ensure section of foo, return_value is 1. It is not a value from block, so Ruby "forgets" about previous return 3 and forgets to return it from bar.
  4. It returns 1 from foo and 4 from bar.

Moreover, if you write next 3 instead of return 3 in the block - it will return to foo after yield and execute puts "s = #{s}"; return 2 even without the ensure block. This is a magical Ruby feature for iterators and enumerators.

Upvotes: 5

Related Questions