Understanding and Leveraging Closure
A Brief Review of Scope
Before we get started with closure, we should do a brief review of scope — the two concepts are, after all, inextricably intertwined. For an in depth look at scope, you can check out this article. The short version is as follows. Scope is composed of a set of nested boundaries that define which variables are accessible at any given point in your code. The top layer of scope is global scope and is universally accessible. Nested within global scope are individual layers of local scope or function scope. When your program attempts to look up the value held in a given variable, it starts at the closest level of scope (often inside a function), and if no such variable exists there, it then moves up to higher levels of scope, checking each one in turn. Note that this is a unidirectional process — higher levels of scope are accessible to lower levels, but not vice versa. Per usual, this is probably easiest to see in code:
Here we have a variable in global scope called
event, which holds the value
“Coffee with Ada.” We also have a function called
calendar, which contains a variable that is also called
event but has the value
“Party at Charles’ house.” When we call the function
calendar, and it goes looking for a variable called
event to log to the console, it finds the variable of that name that is defined in local scope. However, when we attempt to log the value of event from outside of any functions, it finds the variable of that name defined in global scope instead.
Now that we have refreshed our understanding of scope, let’s look at closure. We know that variables are accessible depending on where they are in scope, but what does that mean for our function calls? Well, when you define a function it closes over the currently accessible scope and retains access to the variables defined in that scope. So, if you define a function in global scope, it has access to all of the variables in global scope, but nothing else. On the other hand, if you define a function inside another function (that is, in function scope), then it will have access to the variables in function scope and in global scope. This is where things can get tricky though. Let’s look at an example.
In this snippet we have a global scope variable called
event, a function defined in global scope called
describeEvent, which simply logs the value of
event, as it knows it. We also have another function called
calendar that defines its own
event variable in function scope and then calls
describeEvent. So what then happens when we call the function
calendar? You might expect it to log
“Party at Charles’ house.”, which is the value of the
event variable inside
calendar’s function scope. However, the actual output is
“Coffee with Ada.” The reason is that our
describeEvent function had already closed over the variable
event in global scope. It doesn’t matter that we’re calling
describeEvent from inside calendar. As far as
describeEvent is concerned, the
event variable inside
calendar doesn’t exist. And that’s closure in action!
Closure in Higher-Order Functions
This example defines a function called
makeEventDescriber, which accepts an
event and a
date string as parameters. On being called, the function assigns the provided values to local scope variables named
date. It then returns another function, which closes over those variables and uses them to log a value to the console. After defining this function, we then use it to make two new functions called
Now that we have two new functions,
partyAtCharles, what happens when we call them? Note that neither of them accepts any parameters, and yet, when we call them they output the strings that we expect. So how do they retain access to the
event variables that they depend upon to function properly? And how is it that they have different values? All of this happens because of closure. The functions returned by
makeEventDescriber have closed over the values provided to it when it was called, which is why
partyAtCharles have access to different values.
Fun with Partial Function Application
One of the neat things about closure is that it lets us create functions that have pre-applied parameters. This is called partial function application, and we can use it to make functions that make other functions. For example, what if we knew that we wanted to schedule lots of events with our friend Ada. Wouldn’t it be nice to not have to provide the name “Ada” to every event that we schedule with her? We can do exactly that with partial function application.
Here, we have a function called
schedulerMaker, which has a single parameter that expects a
name string. This function then returns another function, which accepts an
event as its parameter. Finally, this function itself returns a final function, which when called, logs a string describing our event. That’s a lot of functions, so let’s try to follow what is happening.
First, we call
schedulerMaker with the argument
“Ada” and it gives us a new function, which we assign to the variable
adaScheduler. For its part,
adaScheduler closes over the variable
name, which contains the value of
“Ada”, and therefore retains permanent access to it. We then call
adaScheduler with the argument
“Coffee”, which it assigns to the variable
event, and it in turn returns another function that is going to describe our event. Now, we have a function called
coffeeWithAda that has, in effect, closed over two levels of scope, thus giving it access to both the
name variables. And indeed, when we call
coffeeWithAda, the message
“Coffee with Ada” is logged to the console.
Keep in mind that the
name variables we are using here are not universal. If we wanted, we could use
schedulerMaker to create many more functions, each with a different name attached. We could then in turn use those functions to create many more event describers, and each one would have access to different values.
Private Data and Application Interfaces
Two of the biggest benefits of closure are the ability to create private data and to define application interfaces. Sometimes, you want to enforce the way in which a program interacts with data so that you can protect its integrity. By using closure, you can do exactly this. One common way of creating such an interface is by returning an object from a function. Methods defined on this object, just like any other functions, close over the current scope. This means that we can define private data inside the overall function scope, which will only be accessible to methods defined on our object. Here is an example:
In this mini-program we have a function called
makeCalendar which accepts a single parameter (a name of the calendar owner) and returns an object. This object has several methods defined on it to add and list calendar events. These methods, through closure, have access to a private
calendar object that is defined in the overall function scope. This object is only accessible to those methods and not to the program as a whole. We see this in action at the bottom of the snippet where we create and interact with an object called
What is interesting about this pattern is that the
calendar object that
babbageCalendar is interacting with is completely private. There is no way to directly manipulate the
calendar.events array or the
calendar.owner string — we can only do so by using the explicitly defined interface that is returned by
makeCalendar. This can be very powerful, but it has downsides as well. What if we wanted a way to change the date on a particular event? Well, we can only do that by going back to the original code and adding a
changeDate method to the interface. As a result, this pattern is not particularly easy to extend, but for our purposes, it illustrates the power of closure.
And that’s all for today’s exploration of closure! Hopefully this has been a useful review of how closure works and how you can use it effectively. As always, happy coding!