Code

Ruby: Block Parameters and Return Values

posted in: Ruby, Software Engineering | 0

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:

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:

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

You might expect the flow to be this:

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

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:

Looking better, but we didn’t want those two nils 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:

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:

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:

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:

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