Ruby: Block Parameters and Return Values

Ruby: Block Parameters and Return Values

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  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:

  1. Line 2 yields to the implicit block argument, passing the number 1 (first value in ary.)
  2. The block doesn’t return on line 6, because num isn’t greater than 3. Instead, it executes line 7, squaring the number assigned to num and returning the result.
  3. The number 1 gets pushed onto the result array.
  4. Steps 1, 2 and 3 in this flow are repeated for the numbers 2 and 3 from the ary receiver.
  5. When line 2 yields to the block and passes the number 4, line 6 returns.
  6. The test method then returns an array with [1, 4, 9] in it.
  7. Line 5 assigns this returned array to outer_result.
  8. Line 10 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 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:

Ruby: Blocks

Ruby: Scope and Closures

Ruby: Procs, Lambdas and Bindings

To view or add a comment, sign in

More articles by Robert Rodes

Explore content categories