The Foundations of a Closure

Let’s begin with a definition of a closure. A closure is essentially a function whose body references a variable that is declared in the parent scope.

The key to understanding closures is to grasp two other programming language concepts: lexical scoping and free variables. Let’s look at lexical scoping first, and then dive into free variables.

Lexical Scoping: Closest Variable Wins

Lexical scoping rules serve to answer one question: what is the value of this variable at this line? In short, lexical scoping says that whichever assignment to a variable that is the closest gets that value.

The value of a variable x is given by the innermost statement that declares x. Furthermore, the area in a program where a variable maintains a value is called the scope of that variable.

Therefore, you can find out the value of a variable by simply eyeballing a line in your program without having to run it. This should feel intuitive to you; every time you try to work out the value of a variable, you do so by following lexical scoping rules. Lexical scoping is sometimes known by another term— static scoping. I guess these terms are more catchy than “eyeball scoping.”

Let’s take a look at an example. Open irb and follow along with the following example:

 >>​ msg = ​"drive the principal's car"
 =>​ ​"drive the principal's car"
 
 >>​ 3.times ​do
 >>​ prefix = ​"I will not"
 >>​ puts ​"​​#{​prefix​}​​ ​​#{​msg​}​​"
 >>​ ​end
 
 I will not drive the principal's car
 I will not drive the principal's car
 I will not drive the principal's car

do .. end creates a new scope. Within the block, prefix is declared and has the value "I will not". msg is more interesting. It is not declared within the block, but in the outermost, or parent, scope. The inner scope has access to the parent scope. Because of this, msg continues to have the value "drive the principal’s car"

You have just seen that the inner scope has access to the parent scope. Does the opposite hold true? For example, can prefix be accessed outside of the block? You can easily find out using irb again:

 >>​ puts prefix
 NameError: undefined local variable or method `prefix' for main:Object
  from (irb):6

You have just proven that this is definitely not the case; prefix is only visible from within the block, and nowhere else. You might not usually think about it, but this is lexical scoping in action.

You now know what lexical scoping is. The other piece of the puzzle is the free variable, or more precisely, identifying the free variable.

Identifying Free Variables

A free variable is a variable that is defined in a parent scope. Let’s look at an example that will make this clearer. We’ll modify the program we just wrote to use lambdas instead. Lambdas are Ruby’s version of anonymous functions found in other languages. Type this program into your irb session:

 >>​ chalkboard_gag = lambda ​do​ |msg|
 >>​ lambda ​do
 >>​ prefix = ​"I will not"
 >>​ ​"​​#{​prefix​}​​ ​​#{​msg​}​​"
 >>​ ​end
 >>​ ​end
 =>​ ​#<Proc:0x007ffa2f901600@(irb):1 (lambda)>

Instead of seeing the output, the session shows that the return value is a lambda that is represented by a Proc object. Don’t worry about this for now; we’ll come back to this soon. Instead, try to imagine how you would make the chalkboard_gag variable return "drive the principal’s car" It might not be as straightforward as you think!

The trick here is to tease apart chalkboard_gag layer by layer. The first layer is the outermost lambda:

»chalkboard_gag = lambda ​do​ |msg|
  lambda ​do
  prefix = ​"I will not"
 "​​#{​prefix​}​​ ​​#{​msg​}​​"
 end
»end

The outermost lambda takes a single argument, msg. But what does it return? Another lambda. Let’s turn our attention to the inner lambda:

 chalkboard_gag = lambda ​do​ |msg|
» lambda ​do
  prefix = ​"I will not"
 "​​#{​prefix​}​​ ​​#{​msg​}​​"
»end
 end

The body of the inner lambda declares the prefix variable. On the other hand, msg is not declared anywhere in the lambda’s body. Where is it declared then? It’s declared in the parent scope as the argument of the outer lambda. This makes msg a free variable. The parent scope is also called the surrounding lexical scope, because the outer lambda wraps around the inner one. It is this wrapping around that allows the inner lambda to access variables declared in the outer one.

Let’s put chalkboard_gag to the test. Go back to irb where you last created the chalkboard_gag lambda. Then supply a value to the outermost lambda. Invoke the lambda by using the call method:

 >>​ inner_lambda = chalkboard_gag.call(​"drive the principal's car"​)
 =>​ ​#<Proc:0x007fca608589a0@(irb):2 (lambda)>

As expected, the return result is a lambda. To get to the final result, you need to invoke the inner lambda, which you assigned to inner_lambda. To invoke it, use the call method:

 >>​ inner_lambda.call()
 =>​ ​"drive the principal's car"

Whenever an inner lambda refers to a variable that is not declared within it, but that variable is declared in the parent scope of that lambda, that is a free variable.

At this stage, you almost have all the tools and knowledge needed to point out a closure. You know what lexical scoping is and how it works. You also know how to identify a free variable. Now, you need to know what separates closures and non-closures.

Rules of Identifying a Closure

Recall the definition of a closure:

  1. It needs to be a function...

  2. whose body references some variable that...

  3. is declared in a parent scope.

Since Ruby doesn’t have the concept of a traditional function, we’re going to be a bit loose with the definition. In the context of Ruby, this means a block, Proc, or lambda.

However, being a block, Proc, or lambda is not enough. The body must contain at least one variable that is declared in the parent scope. What kind of variable is that? That’s right, a free variable.

At this point, you might be thinking: what interesting things can I do with closures? The answer might surprise you!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.107.193