Acknowledgements
This material is taken verbatim from Software Foundations V4.0
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Assumed Knowledge:
* TODO
Learning Outcomes:
* TODO
Require Import Maps.
Require Import Smallstep.
Require Import Types.
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 arise from the mechanisms of
variable binding and
substitution. It which will take some
work to deal with these.
Overview
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:
- variables
- function abstractions
- application
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
|
true constant true
|
false constant false
|
if t1 then t2 else t3 conditional
The
\ symbol 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
t2 is its
body. The annotation
:T1
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. if x then false else true
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. (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) false true
A two-argument function that takes two booleans and returns
the first one, applied to the booleans false and true.
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 (f true)
A higher-order function that takes a function f (from
booleans to booleans) as an argument, applies f to true,
and applies f again to the result.
- (\f:Bool→Bool. f (f true)) (\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.
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.
For example:
- \x:Bool. false has type Bool→Bool
- \x:Bool. x has type Bool→Bool
- (\x:Bool. x) true has type Bool
- \x:Bool. \y:Bool. x has type Bool→Bool→Bool
(i.e., Bool → (Bool→Bool))
- (\x:Bool. \y:Bool. x) false has type Bool→Bool
- (\x:Bool. \y:Bool. x) false true has type Bool
Syntax
We begin by formalizing the syntax of the STLC.
Note that an abstraction
\x:T.t (formally,
tabs 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...
Definition x := (
Id 0).
Definition y := (
Id 1).
Definition z := (
Id 2).
Hint Unfold x.
Hint Unfold y.
Hint Unfold z.
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. if x then false else true
(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 Gallina,
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.
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.
Substitution
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.
if x then true else x)
false
to
if false then true else false
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."
Here are some examples:
- [x:=true] (if x then x else false)
yields if true then true else false
- [x:=true] x yields true
- [x:=true] (if x then x else y) yields if true then true else y
- [x:=true] y yields y
- [x:=true] false yields false (vacuous substitution)
- [x:=true] (\y:Bool. if y then x else false)
yields \y:Bool. if y then true else false
- [x:=true] (\y:Bool. x) yields \y:Bool. true
- [x:=true] (\y:Bool. y) yields \y:Bool. y
- [x:=true] (\x:Bool. x) yields \x:Bool. x
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.
Here is the definition, informally...
[
x:=
s]
x =
s
[
x:=
s]
y =
y if x ≠
y
[
x:=
s](\
x:
T11.
t12) = \
x:
T11.
t12
[
x:=
s](\
y:
T11.
t12) = \
y:
T11. [
x:=
s]
t12 if x ≠
y
[
x:=
s](
t1 t2) = ([
x:=
s]
t1) ([
x:=
s]
t2)
[
x:=
s]
true =
true
[
x:=
s]
false =
false
[
x:=
s](
if t1 then t2 else t3) =
if [
x:=
s]
t1 then [
x:=
s]
t2 else [
x:=
s]
t3
... 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 avoid 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
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.
☐
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 (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:=
v2]
t12
is traditionally called "beta-reduction".
value v2 |
(ST_AppAbs)
|
|
(\x:T.t12) v2 ⇒ [x:=v2]t12 |
|
t1 ⇒ t1' |
(ST_App1)
|
|
t1 t2 ⇒ t1' t2 |
|
value v1 |
|
t2 ⇒ t2' |
(ST_App2)
|
|
v1 t2 ⇒ v1 t2' |
|
... plus the usual rules for booleans:
|
(ST_IfTrue)
|
|
(if true then t1 else t2) ⇒ t1 |
|
|
(ST_IfFalse)
|
|
(if false then t1 else t2) ⇒ t2 |
|
t1 ⇒ t1' |
(ST_If)
|
|
(if t1 then t2 else t3) ⇒ (if t1' then t2 else t3) |
|
Formally:
Examples
Example:
((\
x:
Bool→Bool.
x) (\
x:
Bool.
x))
⇒* (\
x:
Bool.
x)
i.e.,
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.
if x then false
else true))
true)
⇒* false
i.e.,
((
idBB notB)
ttrue)
⇒* tfalse.
Example:
((\
x:
Bool → Bool.
x) ((\
x:
Bool.
if x then false
else true)
true))
⇒* false
i.e.,
(
idBB (
notB ttrue))
⇒* tfalse.
We can use the
normalize tactic defined in the
Types chapter
to simplify these proofs.
Exercise: 2 stars (step_example3)
Try to do this one both with and without
normalize.
☐
Typing
Next we consider the typing relation of the STLC.
Contexts
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
Γ ⊢ t ∈ T, where
Γ is a
"typing context" — a mapping from variables to their types.
Informally, we'll write
Γ, x:T for "extend the partial
function
Γ to also map
x to
T." Formally, we use the
function
extend to add a binding to a partial map.
Typing Relation
Γ x = T |
(T_Var)
|
|
Γ ⊢ x ∈ T |
|
Γ , x:T11 ⊢ t12 ∈ T12 |
(T_Abs)
|
|
Γ ⊢ \x:T11.t12 ∈ T11->T12 |
|
Γ ⊢ t1 ∈ T11->T12 |
|
Γ ⊢ t2 ∈ T11 |
(T_App)
|
|
Γ ⊢ t1 t2 ∈ T12 |
|
|
(T_False)
|
|
Γ ⊢ false ∈ Bool |
|
Γ ⊢ t1 ∈ Bool Γ ⊢ t2 ∈ T Γ ⊢ t3 ∈ T |
(T_If)
|
|
Γ ⊢ if t1 then t2 else t3 ∈ T |
|
We can read the three-place relation
Γ ⊢ t ∈ T as:
"to the term
t we can assign the type
T using as types for
the free variables of
t the ones specified in the context
Γ."
Note that since we added the has_type constructors to the hints
database, auto can actually solve this one immediately.
Another example:
empty ⊢ \
x:
A. λ
y:
A→A.
y (
y x))
∈
A → (
A→A)
→ A.
Exercise: 2 stars, optional (typing_example_2_full)
Prove the same result without using
auto,
eauto, or
eapply (or
...).
☐
Exercise: 2 stars (typing_example_3)
Formally prove the following typing derivation holds:
empty ⊢ \
x:
Bool→B. λ
y:
Bool→Bool. λ
z:
Bool.
y (
x z)
∈
T.
☐
We can also show that terms are
not typable. For example, let's
formally check that there is no typing derivation assigning a type
to the term
\x:Bool. \y:Bool, x y — i.e.,
¬
∃T,
empty ⊢ \
x:
Bool. λ
y:
Bool,
x y :
T.
Example typing_nonexample_1 :
¬
∃T,
empty ⊢
(
tabs x TBool
(
tabs y TBool
(
tapp (
tvar x) (
tvar y)))) ∈
T.
Proof.
intros Hc.
inversion Hc.
inversion H.
subst.
clear H.
inversion H5.
subst.
clear H5.
inversion H4.
subst.
clear H4.
inversion H2.
subst.
clear H2.
inversion H5.
subst.
clear H5.
inversion H1.
Qed.
Exercise: 3 stars, optional (typing_nonexample_3)
Another nonexample:
¬ (
∃S,
∃T,
empty ⊢ \
x:
S.
x x ∈
T).
☐