Demystify Closures

Part of "Demystify Programming Languages" series

  1. Demystify Programming Languages
  2. Demystify Errors
  3. Demystify Variables
  4. Demystify Functions
  5. Demystify Closures

In the previous post we implemented functions, but not about closures. Let’s fix this.

The problem

Without closures following code snippet doesn’t work as expected:

1> (define getFun
2    (function (x y)
3      (function (i j)
4        (- (+ x y) (+ i j))
5      )
6    )
7  )
8> (define fun (getFun 5 4))
9> (fun 3 2)

It will result in an error (Can't find "y" variable...) but we want it to return 4.

Closures will fix this problem because closure is a function with environment attached to it e.g. (function (i j) ... will have access to local variables of parent function (function (x y).

This kind of variable resolution (nested scopes) is called lexical scope.

The solution

The solution is to store the environment (at which function was created) together with function. The function which comes with environment called closure.

Closures are data structures with both a code and a data component.

Closure conversion: How to compile lambda

For now, we store a function as a list with 3 items: symbol “function”, list of arguments, the body of the function. Let’s store environment as the fourth item:

 1const evaluate = (ast, environment = { ...defaultEnvironment }) => {
 2  // ...
 3  const [name, first, second] = ast;
 4  const numberOfArguments = ast.length - 1;
 5  if (name === "define") {
 6    // ...
 7  } else if (name === "function") {
 8    return [name, first, second, environment];
 9  } else {
10    // ...
11  }
12};

And when we call the function we need to use closure’s environment along with “global” environment:

 1const evaluate = (ast, environment = { ...defaultEnvironment }) => {
 2  // ...
 3  if (name === "define") {
 4    // ...
 5  } else {
 6    // ...
 7    if (isFunction(environment[name])) {
 8      const [_, argumentNames, functionBody, closureEnvironment] = environment[
 9        name
10      ];
11      // create new environment from "global" and closure's environment
12      const functionEnvironment = { ...environment, ...closureEnvironment };
13      // add arguments to environment
14      // ...
15      return evaluate(functionBody, functionEnvironment);
16    }
17    throw new RuntimeError(`"${name}" is not a function`);
18  }
19};

And this is the whole secret behind closures. Note: proposed implementation of environment storage is not the most effective, because we will have a lot of copies of the environment.

Local scope

What would you expect from the usage of define inside a function? The function has its local scope, all variables defined in this scope will stay in this scope (insert joke about Las Vegas here).

1> (define testLocal
2    (function (x)
3      (define local x)
4    )
5  )
6> (testLocal 10)
7> (+ local 1)

This code snippet will produce error Can't find "local" variable....

Encapsulation

Encapsulation (a term often used in Object Oriented Programming) - is the way to provide isolation, for example, to prevent undesired data change, or to hide implementation details.

Closures provide encapsulation as well. Closure carries a piece of the environment which can contain variables nobody else can read (if the closure was created inside another function). In this sense, closure provides a way to isolate a piece of data.

PS

Code for this post is here. In the next post will probably talk about the evaluation strategies.

Except where otherwise noted, content on this site is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0