Module 13: Closures and types

This module is due at 1:15pm on Tuesday, October 25.

In the previous module, you wrote an interpreter for the untyped lambda calculus (extended with integers and addition) by interpreting lambda-calculus integers and functions directly to Haskell Integers and functions. This is in some sense the easiest interpreter to write. However, it had a few problems:

  1. The same implementation technique would not work in another language without first-class functions (say, C). Even in languages like Java or Python that nominally do have first-class functions, it might be nicer to avoid them in this case.
  2. Once we have a function, we cannot use or inspect it in any way other than applying it to arguments. This made generating error messages or the results of the interpreter more difficult, because there is no way to print out a function.

Today we will start by considering another way to implement the interpreter which solves these problems.

Closures

The basic idea is to think of the value of a function as a sort of suspended computation. When the interpreter hits a function, it puts the function into “suspended animation”; when it is finally provided with an argument then computation can resume. So when we encounter a function we don’t try to interpret it any further, but just save its parameter name and body in a closure, to be interpreted later.

However, this is not quite enough. The body of a function might contain variables, which need to be interpreted in an environment. When we “suspend” a function, we need to be sure to save the current environment along with it, so that when we wake it up to continue executing, any variables it contains will be interpreted in the correct environment. We call this combination of a function body and its environment a closure.

Types for the lambda calculus

As you have probably noticed, up until this point it is quite possible to write nonsensical expressions like 3 6, where 3 is applied to the argument 6 as if it were a function. As usual, we’d like to create a type system and type checker which can rule out such nonsensical expressions before running our programs. This turns out to be a bit more subtle than for Arith, and we will spend the next few modules exploring the idea. This module will just get you to start thinking about some of the issues involved.

In general, we need to know not just that something is a function, but also what type it expects as an input, and what type it will return as an output. As in Haskell, we will write in -> out for a function that expects an input of type in and yields a result of type out.

We can now think about how to figure out the type of a lambda expression.

Type annotations

As you probably found, inferring the types of arbitrary lambda expressions requires some nontrivial logical deduction. For now at least, we’re not even going to implement it in its most general form. The main problem arises when we are asked to infer the type of something like (^x -> 3). It is a function which returns an Int, but what is the type of the input? We cannot know without looking to see how the function is used. This makes type inference non-local, and requires doing some kind of constraint solving. This can be done (for example, GHC does it), but it will make things much easier for now if we just refuse to infer the type of a lambda expression like this. Instead, we can extend the syntax of lambda expressions to allow specifying the type of the argument, like this: (^ x [Int] -> 3). This specifies that the argument x has type Int, so we can infer that the whole lambda expression has type Int -> Int.

On the other hand, we don’t always need type annotations on lambdas like this. For example, we can easily check whether (^x -> 3) has a given type. So we will make the type annotations optional. That is, lambda expressions will now have this syntax:

<expr> ::=
  ...
  | '^' <var> ('[' <type> ']')? '->' <expr>

Recall that (...)? means the part inside (...) is optional, i.e. may occur zero or one times.

Feedback