StlcThe Simply Typed Lambda-Calculus

RequireExportTypes.

The Simply Typed Lambda-Calculus

The simply typed lambda-calculus (STLC) is a tiny core calculus
embodying the key concept of functional abstraction, which shows
up in pretty much every real-world programming language in some
form (functions, procedures, methods, etc.).

We will follow exactly the same pattern as in the previous
chapter when formalizing this calculus (syntax, small-step
semantics, typing rules) and its main properties (progress and
preservation). The new technical challenges (which will take some
work to deal with) all arise from the mechanisms of variable
binding and substitution.

Overview

The STLC is built on some collection of base types — booleans,
numbers, strings, etc. The exact choice of base types doesn't
matter — the construction of the language and its theoretical
properties work out pretty much the same — so for the sake of
brevity let's take just Bool for the moment. At the end of the
chapter we'll see how to add more base types, and in later
chapters we'll enrich the pure STLC with other useful constructs
like pairs, records, subtyping, and mutable state.

Starting from the booleans, we add three things:

variables

function abstractions

application

This gives us the following collection of abstract syntax
constructors (written out here in informal BNF notation — we'll
formalize it below).

The λ symbol (backslash, in ascii) in a function abstraction
λx:T1.t2 is generally written as a greek letter "lambda" (hence
the name of the calculus). The variable x is called the
parameter to the function; the term t1 is its body. The
annotation :T specifies the type of arguments that the function
can be applied to.

Some examples:

λx:Bool.x

The identity function for booleans.

(λx:Bool.x)true

The identity function for booleans, applied to the boolean true.

λx:Bool.ifxthenfalseelsetrue

The boolean "not" function.

λx:Bool.true

The constant function that takes every (boolean) argument to
true.

λx:Bool.λy:Bool.x

A two-argument function that takes two booleans and returns
the first one. (Note that, as in Coq, a two-argument function
is really a one-argument function whose body is also a
one-argument function.)

(λx:Bool.λy:Bool.x)falsetrue

A two-argument function that takes two booleans and returns
the first one, applied to the booleans false and true.

Note that, as in Coq, application associates to the left —
i.e., this expression is parsed as ((λx:Bool.λy:Bool.x)false)true.

λf:Bool→Bool.f(ftrue)

A higher-order function that takes a functionf (from
booleans to booleans) as an argument, applies f to true,
and applies f again to the result.

(λf:Bool→Bool.f(ftrue))(λx:Bool.false)

The same higher-order function, applied to the constantly
false function.

As the last several examples show, the STLC is a language of
higher-order functions: we can write down functions that take
other functions as arguments and/or return other functions as
results.

Another point to note is that the STLC doesn't provide any
primitive syntax for defining named functions — all functions
are "anonymous." We'll see in chapter MoreStlc that it is easy
to add named functions to what we've got — indeed, the
fundamental naming and binding mechanisms are exactly the same.

The types of the STLC include Bool, which classifies the
boolean constants true and false as well as more complex
computations that yield booleans, plus arrow types that classify
functions.

Note that an abstraction λx:T.t (formally, tabsxTt) is
always annotated with the type T of its parameter, in contrast
to Coq (and other functional languages like ML, Haskell, etc.),
which use type inference to fill in missing annotations. We're
not considering type inference here, to keep things simple.

(We write these as Notations rather than Definitions to make
things easier for auto.)

Operational Semantics

To define the small-step semantics of STLC terms, we begin — as
always — by defining the set of values. Next, we define the
critical notions of free variables and substitution, which are
used in the reduction rule for application expressions. And
finally we give the small-step relation itself.

Values

To define the values of the STLC, we have a few cases to consider.

First, for the boolean part of the language, the situation is
clear: true and false are the only values. An if
expression is never a value.

Second, an application is clearly not a value: It represents a
function being invoked on some argument, which clearly still has
work left to do.

Third, for abstractions, we have a choice:

We can say that λx:T.t1 is a value only when t1 is a
value — i.e., only if the function's body has been
reduced (as much as it can be without knowing what argument it
is going to be applied to).

Or we can say that λx:T.t1 is always a value, no matter
whether t1 is one or not — in other words, we can say that
reduction stops at abstractions.

Coq, in its built-in functional programming langauge, makes the
first choice — for example,

Evalsimplin (funx:bool => 3 + 4)

yields funx:bool=>7.

Most real-world functional programming languages make the second
choice — reduction of a function's body only begins when the
function is actually applied to an argument. We also make the
second choice here.

Finally, having made the choice not to reduce under abstractions,
we don't need to worry about whether variables are values, since
we'll always be reducing programs "from the outside in," and that
means the step relation will always be working with closed
terms (ones with no free variables).

Free Variables and Substitution

Now we come to the heart of the STLC: the operation of
substituting one term for a variable in another term.

This operation will be used below to define the operational
semantics of function application, where we will need to
substitute the argument term for the function parameter in the
function's body. For example, we reduce

(λx:Bool. ifxthentrueelsex) false

to

iffalsethentrueelsefalse

by substituting false for the parameter x in the body of the
function.

In general, we need to be able to substitute some given
term s for occurrences of some variable x in another term t.
In informal discussions, this is usually written [x:=s]t and
pronounced "substitute x with s in t."

The last example is very important: substituting x with true in
λx:Bool.x does not yield λx:Bool.true! The reason for
this is that the x in the body of λx:Bool.x is bound by the
abstraction: it is a new, local name that just happens to be
spelled the same as some global name x.

Technical note: Substitution becomes trickier to define if we
consider the case where s, the term being substituted for a
variable in some other term, may itself contain free variables.
Since we are only interested here in defining the step relation
on closed terms (i.e., terms like λx:Bool.x, that do not mention
variables are not bound by some enclosing lambda), we can skip
this extra complexity here, but it must be dealt with when
formalizing richer languages.

Exercise: 3 stars (substi)

The definition that we gave above uses Coq's Fixpoint facility
to define substitution as a function. Suppose, instead, we
wanted to define substitution as an inductive relationsubsti.
We've begun the definition by providing the Inductive header and
one of the constructors; your job is to fill in the rest of the
constructors.

Reduction

The small-step reduction relation for STLC now follows the same
pattern as the ones we have seen before. Intuitively, to reduce a
function application, we first reduce its left-hand side until it
becomes a literal function; then we reduce its right-hand
side (the argument) until it is also a value; and finally we
substitute the argument for the bound variable in the body of the
function. This last rule, written informally as