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 arise from the mechanisms of variable binding and substitution. It will take some work to deal with these.
The STLC is built on some collection of base types: booleans, numbers, strings, etc. The exact choice of base types doesn't matter much -- the construction of the language and its theoretical properties work out the same no matter what we choose -- 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 boolean constants and conditionals, we add three things:
This gives us the following collection of abstract syntax constructors (written out first in informal BNF notation -- we'll formalize it below).
t ::= x variable | \x:T1.t2 abstraction | t1 t2 application | tru constant true | fls constant false | test t1 then t2 else t3 conditional
The \ symbol in a function abstraction \x:T.t 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 t is its body. The annotation :T1
specifies the type of arguments that the function can be applied
to.
Some examples:
The identity function for booleans.
The identity function for booleans, applied to the boolean tru.
The boolean not
function.
The constant function that takes every (boolean) argument to tru.
A two-argument function that takes two booleans and returns the first one. (As in Coq, a two-argument function is really a one-argument function whose body is also a one-argument function.)
A two-argument function that takes two booleans and returns the first one, applied to the booleans fls and tru.
As in Coq, application associates to the left -- i.e., this expression is parsed as ((\x:Bool. \y:Bool. x) fls) tru.
A higher-order function that takes a function f (from booleans to booleans) as an argument, applies f to tru, and applies f again to the result.
The same higher-order function, applied to the constantly fls 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.
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 tru and fls as well as more complex computations that yield booleans, plus arrow types that classify functions.
T ::= Bool | T -> T
For example:
We next formalize the syntax of the STLC.
Note that an abstraction \x:T.t (formally, abs x T t) 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.
Some examples...
idB = \x:Bool. x
idBB = \x:Bool->Bool. x
idBBBB = \x:(Bool->Bool) -> (Bool->Bool). x
k = \x:Bool. \y:Bool. x
notB = \x:Bool. test x then fls else tru
(We write these as Notations rather than Definitions to make things easier for auto.)
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.
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: tru and fls are the only values. A test expression is never a value.
Second, an application is 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:
Our usual way of evaluating expressions in Coq makes the first choice -- for example,
Compute (fun x:bool => 3 + 4)
yields:
fun x: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, we must consider what constitutes a complete program.
Intuitively, a complete program
must not refer to any undefined
variables. We'll see shortly how to define the free variables
in a STLC term. A complete program is closed -- that is, it
contains no free variables.
(Conversely, a term with free variables is often called an open term.)
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.
Now we come to the heart of the STLC: the operation of substituting one term for a variable in another term. This operation is 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. test x then tru else x) fls
to
test fls then tru else fls
by substituting fls 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 s for x in t.
Here are some examples:
The last example is very important: substituting x with tru in \x:Bool. x does not yield \x:Bool. tru! 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.
Here is the definition, informally...
x:=sx = s x:=sy = y if x <> y x:=s(\x:T11. t12) = \x:T11. t12 x:=s(\y:T11. t12) = \y:T11. x:=st12 if x <> y x:=s(t1 t2) = (x:=st1) (x:=st2) x:=stru = tru x:=sfls = fls x:=s(test t1 then t2 else t3) = test x:=st1 then x:=st2 else x:=st3
... and formally:
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 include binders for all of the variables they mention), we can sidestep this extra complexity, but it must be dealt with when formalizing richer languages.
For example, using the definition of substitution above to
substitute the open term s = \x:Bool. r, where r is a free
reference to some global resource, for the variable z in the
term t = \r:Bool. z, where r is a bound variable, we would get
\r:Bool. \x:Bool. r, where the free reference to r in s has
been captured
by the binder at the beginning of t.
Why would this be bad? Because it violates the principle that the names of bound variables do not matter. For example, if we rename the bound variable in t, e.g., let t' = \w:Bool. z, then [x:=s]t' is \w:Bool. \x:Bool. r, which does not behave the same as [x:=s]t = \r:Bool. \x:Bool. r. That is, renaming a bound variable changes how t behaves under substitution.
See, for example, Aydemir 2008 (in Bib.v) for further discussion of this issue.
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 relation substi. 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 and prove that the relation you've defined coincides with the function given above.
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 (the function) until it becomes an abstraction; 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 abstraction. This last rule, written informally as
(\x:T.t12) v2 --> x:=v2t12
is traditionally called beta-reduction.
value v2
t1 --> t1'
value v1 t2 --> t2'
... plus the usual rules for conditionals:
t1 --> t1'
Formally:
Example:
(\x:Bool->Bool. x) (\x:Bool. x) -->* \x:Bool. x
i.e.,
idBB idB -->* idB
Example:
(\x:Bool->Bool. x) ((\x:Bool->Bool. x) (\x:Bool. x)) >* \x:Bool. x
i.e.,
(idBB (idBB idB)) -->* idB.
Example:
(\x:Bool->Bool. x) (\x:Bool. test x then fls else tru) tru >* fls
i.e.,
(idBB notB) tru -->* fls.
Example:
(\x:Bool -> Bool. x) ((\x:Bool. test x then fls else tru) tru) >* fls
i.e.,
idBB (notB tru) -->* fls.
(Note that this term doesn't actually typecheck; even so, we can ask how it reduces.)
We can use the normalize tactic defined in the Smallstep chapter to simplify these proofs.
Try to do this one both with and without normalize.
Next we consider the typing relation of the STLC.
Question: What is the type of the term x y
?
Answer: It depends on the types of x and y!
I.e., in order to assign a type to a term, we need to know what assumptions we should make about the types of its free variables.
This leads us to a three-place typing judgment, informally
written Gamma |- t \in T, where Gamma is a
typing context
-- a mapping from variables to their types.
Following the usual notation for partial maps, we write (X |->
T11, Gamma) for update the partial function Gamma so that it
maps x to T.
Gamma x = T
(x |-> T11 ; Gamma) |- t12 \in T12
Gamma |- t1 \in T11->T12 Gamma |- t2 \in T11
Gamma |- t1 \in Bool Gamma |- t2 \in T Gamma |- t3 \in T
We can read the three-place relation Gamma |- t \in T as:
under the assumptions in Gamma, the term t has the type T.
Note that, since we added the has_type constructors to the hints database, auto can actually solve this one immediately.
More examples:
empty |- \x:A. \y:A->A. y (y x) \in A -> (A->A) -> A.
Prove the same result without using auto, eauto, or eapply (or ...).
Formally prove the following typing derivation holds:
empty |- \x:Bool->B. \y:Bool->Bool. \z:Bool. y (x z) \in T.
We can also show that some terms are not typable. For example, let's check that there is no typing derivation assigning a type to the term \x:Bool. \y:Bool, x y -- i.e.,
~ exists T, empty |- \x:Bool. \y:Bool, x y \in T.
Another nonexample:
~ (exists S T, empty |- \x:S. x x \in T).