Methods With an Explicit Block Parameter
A method that uses an explicit block parameter has these rules:
- Only one block parameter is allowed.
- It must be last parameter in the list.
- Its name must begin with
&
.
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 2 3 4 5 6 7 8 9 10 11 |
def test(&block) p block block.call end test { puts "Hi, I'm a block."} #=> #<Proc:0x00007fb173978e30@test.rb:6> #=> Hi, I'm a block. |
Line 3 shows that the block
variable has a Proc
object assigned to it, and line 4 invokes the block.
Proc Return Values
Procs return values in a similar manner to methods. For example:
1 2 3 4 5 6 7 8 |
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 2 3 4 5 6 7 8 9 10 11 12 |
def test(ary) ary.each_with_object([]) { |num, result| result << yield(num) } end outer_result = test([1, 2, 3, 4, 5]) do |num| return if num > 3 num**2 end p outer_result #=> ?? |
You might expect the flow to be this:
- Line 3 yields to the implicit block argument, passing the number 1 (first value in
ary
.) - The block doesn’t return, because
num
isn’t greater than 3. Instead, it squares the number and returns it. - The number
1
gets pushed onto theresult
array. - Same for numbers
2
and3
from theary
receiver. - When line 3 yields to the block and passes the number
4
, line 7 returns. #test
then returns an array with[1, 4, 9]
in it.- Line 6 assigns this returned array to
outer_result
. - Line 11 prints the array.
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 so it never gets assigned to outer_result
. And line 11 never gets executed.
This is because the return
on line 7 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 7 and the #p
method on line 11 never gets executed.
Using break
Suppose we try using break
instead of return
:
1 2 3 4 5 6 7 8 9 10 11 12 |
def test(ary) ary.each_with_object([]) { |num, result| result << yield(num) } end outer_result = test([1, 2, 3, 4, 5]) do |num| break num if num > 3 num**2 end p outer_result #=> 4 |
Not quite what we wanted, either. We manage to execute line 11, but we fail to return the array from the call to test
.
Like the return
statement, the break
statement 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
method on line 11 prints that value.
Using a Lambda to Avoid Return Issues
If we wanted to print the array at the end, we would need to add another parameter to test
and pass a lambda to it:
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 2 3 4 5 6 7 8 9 10 11 12 |
def test(ary, block) ary.each_with_object([]) { |num, result| result << block.call(num) } end p test([1, 2, 3, 4, 5], lambda do |num| return if num > 3 num**2 end) #=>[1, 4, 9, nil, nil] |
Looking better, but we didn’t want those two nil
s in our array. We get them because now with a lambda, the return
on line 7 returns a nil
to the block.call
if num
is greater than 3.
One way to remove nil
values is that is simply to use the #compact
method at the end of the block argument:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def test proc { puts 'Inside block'; return }.call puts 'Inside the method, after running block' 'Return value from #test' end p test puts 'In main, after running test' #=> Inside block #=> nil #=> 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 4 and 5 don’t get executed. Since the return
inside the block doesn’t return a value, test
returns nil
, so line 8 prints nil
. Line 9 then executes, and then the program ends.
Using a Lambda
Finally, here’s one with a lambda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def test(block) block.call puts 'Inside the method, after running block' 'Return value from #test' end p test lambda { puts 'Inside block'; return } puts 'In main, after running test' #=> Inside block #=> Inside the method, after running block #=> "Return value from #test" #=> 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 3. So, lines 4 and 5 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:
Ruby: Blocks
Ruby: Scope and Closures
Ruby: Procs, Lambdas and Bindings