Ruby: Block Parameters and Return Values
Methods With an Explicit Block Parameter
A method that uses an explicit block parameter has these rules:
The & tells Ruby to convert the argument to a Proc object and assign the result to the variable name (minus the &). It does this by calling the Symbol#to_proc method. So, a block argument executes by invoking its Proc#call method.
For example:
1 def test(&block)
2 p block
3 block.call
4 end
5
6 test { puts "Hi, I'm a block."}
7
8 #=> #<Proc:0x00007fb173978e30@test.rb:6>
9 #=> Hi, I'm a block.
Line 2 shows that the block variable has a Proc object assigned to it, and line 3 invokes the block.
Proc Return Values
Procs return values in a manner similar to methods. For example:
def test(ary, block)
ary.each_with_object([]) { |num, result| result << block.call(num) }
end
p test([1, 2, 3, 4, 5], proc { |num| num**2 }) #=> [1, 4, 9, 16, 32]
p test([1, 2, 3, 4, 5], lambda { |num| num**2 }) #=> [1, 4, 9, 16, 32]
Problems With Using return in Blocks
However, blocks and methods can diverge a great deal in how they return values when program flow is interrupted. In fact, using break or return in a block can have some pretty inconsistent results. For example, here’s a “gotcha”:
1 def test(ary)
2 ary.each_with_object([]) { |num, result| result << yield(num) }
3 end
4
5 outer_result = test([1, 2, 3, 4, 5]) do |num|
6 return if num > 3
7 num**2
8 end
9
10 p outer_result #=> ??
You might expect the flow to be this:
But this isn't what happens. This code actually fails to print anything at all! The first five steps are correct, but the array never gets returned and never gets assigned to outer_result. And line 10 never gets executed.
This is because the return executes in the context of the block’s closure. That context is the context in which the block was created — its lexical scope. In this case, that’s main. And in main, a return statement immediately exits the entire program, so the program exits on line 6 and the p method on line 10 never gets executed.
Using break
Suppose we try using break instead of return on line 6:
1 def test(ary)
2 ary.each_with_object([]) { |num, result| result << yield(num) }
3 end
4
5 outer_result = test([1, 2, 3, 4, 5]) do |num|
6 break if num > 3
7 num**2
8 end
9
10 p outer_result #=> 4
Not quite what we wanted, either. We manage to execute line 10, but fail to return the array from the call to test.
Like the return statement, the break statement also executes in the context of the block's lexical scope, which is main. So, execution goes straight back to main, carrying the current value of num along. That value is 4, so 4 gets assigned to outer_result. Then the p on line 10 prints that value.
Using a Lambda to Avoid Return Issues
If we want to print the array at the end, we have to use a lambda. Lambdas can't be implicitly passed (trying to put the lambda keyword in front of the block results in a syntax error), so we need to add a second parameter to test (here, block on line 1) and pass a lambda to it:
1 def test(ary, block)
2 ary.each_with_object([]) { |num, result| result << block.call(num) }
3 end
4
5 p test([1, 2, 3, 4, 5], lambda do |num|
6 return if num > 3
7 num**2
8 end)
9
10 #=>[1, 4, 9, nil, nil]
Well, we didn’t want those two nils in our array. We get them because now with a lambda, the return on line 6 returns a nil to the block.call on line 2 if num is greater than 3. One way to fix that is to simply use the compact method at the end to remove the nils:
def test(ary, block)
ary.each_with_object([]) { |num, result| result << block.call(num) }.compact
end
Summary of Return Behavior
Let's see if we can summarize how blocks, procs and lambdas handle return values.
Returning From an Implicit Block Argument
First, here's what happens when you use return from a passed-in block:
def test
yield
puts 'Inside the method, after running block'
'Return value from #test'
end
p test { puts 'Inside block'; return }
puts 'In main, after running test1'
#=>Inside block
In this, the return executes in the context of main, so return immediately exits the program. No lines execute after the yield statement.
Creating the Block Inside the Method
This one is a little bit different:
1 def test
2 proc { puts 'Inside block'; return }.call
3 puts 'Inside the method, after running block'
4 'Return value from #test'
5 end
6
7 p test
8 puts 'In main, after running test'
9
10 #=> Inside block
11 #=> nil
12 #=> In main, after running test
Here, we create the block inside the method, rather than passing it in implicitly as in the first example. That means that the block's lexical scope is the method rather than main. So return immediately exits the method, just as if it were in the method itself. Program execution continues from there. So, lines 3 and 4 don't get executed. Since the return doesn't return a value, test returns nil, so line 7 prints nil. Line 8 then executes, and then the program ends.
Using a Lambda
Finally, here's one with a lambda:
1 def test(block)
2 block.call
3 puts 'Inside the method, after running block'
4 'Return value from #test'
5 end
6
7 p test lambda { puts 'Inside block'; return }
8 puts 'In main, after running test'
9
10 #=> Inside block
11 #=> Inside the method, after running block
12 #=> "Return value from #test"
13 #=> In main, after running test
Again, a return from a lambda returns to the context in which it is called, rather than the context in which it is created. So when the block invocation encounters a return statement, execution continues from the line after the one where the call method is invoked on line 2. So, lines 3 and 4 get executed.
A lambda's return behavior, then, does not depend on its lexical scope, and is analogous to that of methods. So as a general rule, if it's necessary to interrupt program flow in a block with break or return, it's probably best to use a lambda.
Related Articles
This article is one of a series of four. Here are the other three: