Code

Ruby: Procs, Lambdas and Bindings

posted in: Ruby, Software Engineering | 0

Procs

Yielding to blocks from methods is the simplest way to access closures. However, yield is a bit limited, because it can only invoke the block directly. It can’t send the block somewhere else to be invoked. To do that, you need a Proc object.

The Proc object wraps a block in a first-class function context. First class functions can be:

  • Passed as an argument
  • Returned as a value
  • Assigned to another variable

The Proc object exposes a call method that invokes its block.

To create a Proc object, you can use either of these two syntax variations:

Convention is to use proc (and {}) with single-line blocks, and Proc.new (and do end) with multi-line blocks.

Here’s a bit of code that will show that procs are closures and that we can pass them as an argument:

Line 7 invokes the Proc#call method. This invokes the proc’s block and executes the puts message command. Although the #test method cannot directly see the message variable, the Proc object assigned to the a_proc parameter can. This is because blocks are closures, and the message variable is in the block’s lexical scope. The lexical context is the context from the written standpoint. Since we initialize message on line 10, the block on line 11 can see it. Since the block as written can see message, message is in the block’s lexical scope.

We make this distinction because we actually invoke the block on line 7, where the interior of the #test_proc method’s scope is in force. This is not the same scope as the main area, so when we say that the block carries with it its lexical scope, we are saying that the block has the scope of where we create it rather than the scope of where we invoke it.

Lambdas

A lambda is a specific type of Proc object. To create a lambda, you can use either of two syntax variations:

Convention is to use -> (and {}) with single-line blocks, and lambda (and do end) with multi-line blocks.

This code shows that lambdas are a specific type of Proc object:

Both the proc and the lambda are Proc object instances. The only difference is that in the lambda, the lambda flag is set. The Proc object exposes a #lambda? method that returns true if the Proc instance is a lambda.

Lambdas behave differently from regular Proc objects in two ways:

  1. Doing a return from a block wrapped in a Proc returns in the context in which the Proc was created. Doing a return from a block wrapped in a lambda returns to the context in which the lambda was called.
  2. Procs don’t check whether the #call method passes the right number of arguments to the block. Any missing arguments are assigned the value of nil, and any extra arguments are ignored. Lambdas throw an ArgumentError if #call passes the wrong number of arguments.
Difference One

Let’s look at difference number one first. Here’s how the Proc object behaves:

The return on line 11 returns out of the entire program, so lines 4 and 15 never run.

Now, let’s look at how the same code behaves when written as a lambda:

With a lambda, the return on line 11 returns to line 4, the line after the lambda proc call, and execution continues from there. Therefore, lines 4 and 15 run.

Difference Two

Now, let’s look at the second difference. Consider this code:

From this, you can see that the Proc objects created with Proc::new assign nil to missing arguments and silently ignore extra ones, while objects created with Kernel::lambda throw an ArgumentError when invoking #call with the wrong number of arguments.

Bindings

Consider this code:

Notice that the closure msg_writer, even after it has been created, keeps track of the change to the enclosed variable message.

How? Well, in Ruby, everything is an object. Perhaps we can just keep track of a reference to the message object? Let’s see:

Well, no we can’t. In Ruby, everything is an object, but also in Ruby, every time you reassign a variable, Ruby creates a new object. That’s why lines 7 and 12 have different object ids. So there has to be another mechanism to create closures. That mechanism is a binding.

Binding internals

At a low level, a binding is an object that wraps a stack frame.

A frame is a C struct. A stack frame is a frame whose members contain the state of one of the contexts that is currently executing. For example, in the above code, we have the state that is visible to #test_proc, and we have the state that is visible to main. These two sets of state are in two different frames.

When main or a method executes, a frame gets pushed onto the stack. When execution completes, the frame gets popped off of the stack.

This mechanism won’t work for closures. One frame on the stack can’t see another one, and the closure’s state has to persist beyond the context of the frame in which it’s created. What will work is wrapping the frame in an object and storing it on the heap. An object has a life of its own, so a frame wrapped in an object will persist even when the lifetime of the context that spawned the frame ends. This is why all blocks have a binding associated with them.

The Binding Class

The Binding class defines the binding. Whenever a block is created, an instance of the Binding class (called, helpfully, binding) is created to go along with it. This object keeps track of any changes to the state of the closure. As the doc says, “Objects of class Binding encapsulate the execution context at some particular place in the code and retain this context for future use.” When there is a request for any value contained in a block’s closure, the request is passed to the binding, which locates and accesses the value.

We can use the Binding object to get a bit of insight into how the binding works.

We can see from this code that the Binding object exposes the local variables as symbols with the same name as the variables (including — as always — a reference to self, which in this case is :a_proc).

Related Articles

This article is one of a series of four. Here are the other three:
Ruby: Blocks
Ruby: Scope and Closures
Ruby: Block Parameters and Return Values