5. Stlc: The Simply Typed Lambda-Calculus
The simply typed lambda-calculus (STLC) is a tiny core calculus embodying the key concept of functional abstraction. This concept 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 lives in the lower-left front corner of the famous lambda cube (also called the Barendregt Cube), which visualizes three sets of features that can be added to its simple core:
Moving from bottom to top in the cube corresponds to adding
polymorphic types like ∀ α : Type, α → α. Adding just
polymorphism gives us the famous Girard-Reynolds calculus, System F.
Moving from front to back corresponds to adding type operators
like List.
Moving from left to right corresponds to adding dependent types
like ∀ n m : Nat, n = m.
The top right corner on the back, which combines all three features, is called the Calculus of Constructions. First studied by Coquand and Huet, it forms the foundation of Lean's logic.
5.1. 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 definition of the language as well as
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. In the next 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) for STLC terms t.
| t | ::= | x | (variable) |
| | | λ x : T . t | (abstraction) | |
| | | t t | (application) | |
| | | true | (constant true) | |
| | | false | (constant false) | |
| | | if t then t else t | (conditional) |
The Greek letter λ ("lambda") in a function abstraction λx:T. t is what gives
the calculus its name. The variable x is called the parameter to the
function; the term t is its body. The annotation :T
specifies the type of arguments that the function can be applied to.
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 (as is the case in Lean).
| T | ::= | Bool | |
| | | T → T |
Some examples of STLC terms:
-
λ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 Lean, a two-argument function in the lambda-calculus 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
falseandtrue.(As in Lean, 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, appliesftotrue, and appliesfagain to the result.-
(λf:Bool → Bool. f (f true)) (λx:Bool. false) The same higher-order function, applied to the constantly
falsefunction.
The last two 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: i.e., all functions are "anonymous." We'll see in chapter
MoreStlc that it is easy to add named functions — indeed, the
fundamental naming and binding mechanisms are exactly the same.
Now reconsider our examples, each along with its type:
-
λx:Bool. xhas typeBool → Bool -
(λx:Bool. x) truehas typeBool -
λx:Bool. if x then false else truehas typeBool → Bool -
λx:Bool. truehas typeBool → Bool -
λx:Bool. λy:Bool. xhas typeBool → Bool → Bool(i.e.,Bool → (Bool → Bool)) -
(λx:Bool. λy:Bool. x) false truehas typeBool
The last two, higher-order examples are left off the list on purpose — working out their types is the subject of the quizzes that follow.
What is the type of the following term?
λf:Bool → Bool. f (f true)
(A) Bool → (Bool → Bool)
(B) (Bool → Bool) → Bool
(C) Bool → Bool
(D) Bool
(E) none of the above
How about the type of this one?
(λf:Bool → Bool. f (f true)) (λx:Bool. false)
(A) Bool → (Bool → Bool)
(B) (Bool → Bool) → Bool
(C) Bool → Bool
(D) Bool
(E) none of the above
5.2. Syntax
We next formalize the syntax of the STLC.
namespace Stlc
open scoped MyGetElem
5.2.1. Types
inductive Ty where
| bool
| arrow (τ₁ τ₂ : Ty)
5.2.2. Terms
inductive Tm where
| var (x : String)
| app (t₁ t₂ : Tm)
| abs (x : String) (τ : Ty) (t : Tm)
| tru
| fls
| ite (c t e : Tm)
The constructors above give us a precise representation of STLC syntax, but expressions written directly with them quickly become hard to read. We need some notation magic to set up the concrete syntax, as we did in the Types chapter...
We will write STLC syntax inside <{ ... }> brackets. For example,
<{ λ X : Bool . X }> represents the term
Tm.abs "X" Ty.bool (Tm.var "X").
This notation must also support definitions and proofs about arbitrary piece of STLC
syntax. For example, a theorem may introduce Lean variables t : Tm and
τ : Ty, representing an arbitrary STLC term and type. Inside the brackets,
we can then write <{ λ X : τ . t }>.
Here X is the name of a variable in the
STLC term being represented, while τ and t refer to the Lean variables in
the surrounding theorem.
The notation distinguishes these two uses by naming convention, which we will follow throughout the STLC chapters:
-
A name beginning with a capital Latin letter is taken literally as a name in the STLC syntax. Thus
X,Y, andZare STLC term variables. Such a name must be a single Lean identifier and cannot contain a dot. In languages with named base types, which we will see in the Sub chapter, names such asA,Int, andBoolname those types. -
A name beginning with a lowercase letter or a Greek letter refers to a Lean variable in the surrounding definition or proof. This lets us use the usual names
xandyfor strings,tandufor terms,τfor types, andΓfor contexts without additional punctuation. -
To insert a larger Lean expression, prefix it with
~. For example,<{ ~(Tm.var "X") t }>inserts the expressionTm.var "X"as the function and the Lean variabletas its argument. The same escape is needed to insert a capitalized Lean variable, since an unescaped capitalized name is assumed to be an STLC name.
This capitalization convention applies to actual variable names in concrete
STLC examples and inside <{ ... }> brackets. In grammars, inference rules,
and general explanations, symbols such as x, t, and T instead stand for
an arbitrary variable name, term, or type. We keep the conventional lowercase
notation for these schematic symbols.
Notation encoding
syntax:50 "if " stlcTm:51 " then " stlcTm:50 " else " stlcTm:50 : stlcTm
namespace Elab
open StlcCommon
open Lean Meta Elab Term
def language : Language where
tyType := ``Ty
tmType := ``Tm
arrowCtor := ``Ty.arrow
varCtor := ``Tm.var
appCtor := ``Tm.app
absCtor := ``Tm.abs
-- defined later
subst := `Stlc.subst
hasType := `Stlc.HasType
def boolTyHandler : TyElabHandler :=
fun _recur k T => do
match T with
| `(stlcTy| Bool) =>
return mkConst ``Ty.bool
| _ => k T
def tyHandlers : TyElabHandler :=
boolTyHandler.orElse (commonTyHandler language)
partial def elabTy : TyElab :=
tyHandlers elabTy <| unsupportedTy language
def boolTmHandler : TmElabHandler :=
fun recur k Γ free t => do
match t with
| `(stlcTm| true) => do
return (mkConst ``Tm.tru, free)
| `(stlcTm| false) => do
return (mkConst ``Tm.fls, free)
| `(stlcTm| Bool) => do
throwError "`Bool` is not a valid term."
| `(stlcTm| if $c:stlcTm then $t:stlcTm else $e:stlcTm) => do
let (c, free) ← recur Γ free c
let (t, free) ← recur Γ free t
let (e, free) ← recur Γ free e
return (mkApp3 (mkConst ``Tm.ite) c t e, free)
| _ => k Γ free t
def tmHandlers : TmElabHandler :=
boolTmHandler.orElse (commonTmHandler language elabTy)
partial def elabTm : TmElab :=
tmHandlers elabTm unsupportedTm
def elabCtx : CtxElab := elabCtxCommon language elabTy
@[scoped term_elab StlcCommon.bracket]
def elabBracket : TermElab :=
fun stx expectedType? => do
let `(<{ $q:stlcQuoted }>) := stx
| throwUnsupportedSyntax
elabQuoted language elabTy elabTm elabCtx q expectedType?
end Elab
open scoped Elab
namespace Delab
open StlcCommon Delab
open Lean PrettyPrinter Delaborator
@[app_unexpander Ty.bool]
private def Ty.unexpandBool : Unexpander
| _ => do
let T ← `(stlcTy| $(mkIdent `Bool):ident)
`(<{ $T:stlcTy }>)
@[app_unexpander Ty.arrow]
private def Ty.unexpandArrow : Unexpander := Delab.unexpandArrow
@[app_unexpander Tm.tru]
private def Tm.unexpandTru : Unexpander
| _ => do
let t ← `(stlcTm| $(mkIdent `true):ident)
`(<{ $t:stlcTm }>)
@[app_unexpander Tm.fls]
private def Tm.unexpandFls : Unexpander
| _ => do
let t ← `(stlcTm| $(mkIdent `false):ident)
`(<{ $t:stlcTm }>)
private def reservedNames : String → Bool
| "true" | "false" | "Bool" => true
| _ => false
@[app_unexpander Tm.var]
private def Tm.unexpandVar : Unexpander := Delab.unexpandVar reservedNames ``Tm.var
@[app_delab Tm.var]
private def Tm.delabVar : Delab := Delab.delabVar ``Tm.var
@[app_unexpander Tm.app]
private def Tm.unexpandApp : Unexpander := Delab.unexpandApp
@[app_unexpander Tm.abs]
private def Tm.unexpandAbs : Unexpander := Delab.unexpandAbs
@[app_unexpander Tm.ite]
private def Tm.unexpandIte : Unexpander
| `($_ $c $t $e) =>
`(<{ if $(getTm c) then $(getTm t) else $(getTm e) }>)
| _ => throw ()
end Delab
Here are the terms we will use as running examples, written in the new notation:
abbrev idB := <{ λ X : Bool . X }>
abbrev idBB := <{ λ X : Bool → Bool . X }>
abbrev idBBBB := <{ λ X : (Bool → Bool) → (Bool → Bool) . X }>
abbrev k := <{ λ X : Bool . λ Y : Bool . X }>
abbrev notB := <{ λ X : Bool . if X then false else true }>
Note that an abstraction λ x : T . t (formally, Tm.abs applied to
x, T, and t) is
always annotated with the type T of its parameter, in contrast
to Lean (and other functional languages like ML, Haskell, etc.),
which use type inference to fill in missing annotations. We're
not considering type inference at all here.
5.3. 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.
5.3.1. 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 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. tis a value only whentis 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. tis always a value, no matter whethertis one or not — in other words, we can say that reduction stops at abstractions.
Our usual way of evaluating expressions in Lean makes the first choice — for example,
#reduce fun _x : Bool => 3 + 4
yields:
fun _x => 7
But Lean is rather unusual in this respect. Most 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.
inductive Tm.IsValue : Tm → Prop where
| abs (x : String) (τ₂ : Ty) (t₁ : Tm) : IsValue <{ λ x : τ₂ . t₁ }>
| tru : IsValue <{ true }>
| fls : IsValue <{ false }>
attribute [StlcEval] Tm.IsValue.abs Tm.IsValue.tru Tm.IsValue.fls
The example terms named above are all abstractions, hence all values. We record that once each, so that the reduction examples can cite the fact by name instead of unfolding the definition again at every use.
theorem idB_value : Tm.IsValue idB := .abs ..
theorem idBB_value : Tm.IsValue idBB := .abs ..
theorem notB_value : Tm.IsValue notB := .abs ..
5.3.2. STLC Programs
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, then, is one that is closed — that is, that contains no free variables.
(Conversely, a term that may contain 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.
5.3.3. 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.
Informally, this is written [x:=s]t and pronounced "substitute
s for x in t."
Here are some examples:
-
[X:=true] (if X then true else false)yieldsif true then true else false -
[X:=true] Xyieldstrue -
[X:=true] (if X then X else Y)yieldsif true then true else Y -
[X:=true] YyieldsY -
[X:=true] falseyieldsfalse(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 illuminating: 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:T. t) = λx:T. t
[x:=s](λy:T. t) = λy:T. [x:=s]t if x ≠ y
[x:=s](t₁ t₂) = ([x:=s]t₁) ([x:=s]t₂)
[x:=s]true = true
[x:=s]false = false
[x:=s](if t₁ then t₂ else t₃) =
if [x:=s]t₁ then [x:=s]t₂ else [x:=s]t₃
... and formally:
def subst (x : String) (s : Tm) (t : Tm) : Tm :=
match t with
| .var y =>
if x = y then s else t
| .abs y τ t₁ =>
if x = y then t else <{ λ y : τ . [x := s] t₁ }>
| .app t₁ t₂ =>
<{ ([x := s] t₁) ([x := s] t₂) }>
| .tru => .tru
| .fls => .fls
| .ite t₁ t₂ t₃ =>
<{ if [x := s] t₁ then [x := s] t₂ else [x := s] t₃ }>
Note that due to an unfortunate limitation of Lean's notation system, we must use constructor names for match statements rather than our custom syntax.
Notation encoding
open Lean PrettyPrinter in
@[app_unexpander subst]
def unexpandSubst : Unexpander := StlcCommon.Delab.unexpandSubst
As we did for the evaluators in the Slang chapter, we pair the
definition with one simplification lemma per constructor, saying how subst
behaves on that constructor. The variable and abstraction cases
each need two lemmas, since substitution treats a bound name differently
depending on whether it is the name being substituted for.
variable (x y : String) (s t t₁ t₂ t₃ : Tm) (τ : Ty)
@[simp] theorem subst_var_eq : <{ [x := s] ~(Tm.var x) }> = s := x:Strings:Tm⊢ <{ [~x := s] ~(Stlc.Tm.var x) }> = s
All goals completed! 🐙
@[simp] theorem subst_var_ne (h : x ≠ y) : <{ [x := s] ~(Tm.var y) }> = .var y := x:Stringy:Strings:Tmh:x ≠ y⊢ <{ [~x := s] ~(Stlc.Tm.var y) }> = Stlc.Tm.var y
All goals completed! 🐙
@[simp] theorem subst_abs_eq : <{ [x := s] (λ x : τ . t) }> = <{ λ x : τ . t }> := x:Strings:Tmt:Tmτ:Ty⊢ <{ [~x := s] (λ ~x : τ . t) }> = <{ λ ~x : τ . t }>
All goals completed! 🐙
@[simp] theorem subst_abs_ne (h : x ≠ y) :
<{ [x := s] (λ y : τ . t) }> = <{ λ y : τ . [x := s] t }> := x:Stringy:Strings:Tmt:Tmτ:Tyh:x ≠ y⊢ <{ [~x := s] (λ ~y : τ . t) }> = <{ λ ~y : τ . [~x := s] t }>
All goals completed! 🐙
@[simp] theorem subst_app :
<{ [x := s] (t₁ t₂) }> = <{ ([x := s] t₁) ([x := s] t₂) }> := rfl
@[simp] theorem subst_tru : <{ [x := s] true }> = <{ true }> := rfl
@[simp] theorem subst_fls : <{ [x := s] false }> = <{ false }> := rfl
@[simp] theorem subst_ite :
<{ [x := s] (if t₁ then t₂ else t₃) }> =
<{ if [x := s] t₁ then [x := s] t₂ else [x := s] t₃ }> := rfl
What is the result of the following substitution?
[X:=s](λY:T₁. X (λX:T₂. X))
(1) (λY:T₁. X (λX:T₂. X))
(2) (λY:T₁. s (λX:T₂. s))
(3) (λY:T₁. s (λX:T₂. X))
(4) none of the above
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.
We say that s is an open term.
Here is an example. Using the above definition to substitute the open term
s = λX:Bool. R
(where R is a free reference to some global resource) for
the free 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 [Z:=s]t' is
λW:Bool. λX:Bool. R
which does not behave the same as the substituting in the original t:
[Z:=s]t = λR:Bool. λX:Bool. R
That is, renaming a bound variable in t would change how t
behaves under our simple substitution. So substitution gets more
complicated in that setting, but fortunately we don't have that
problem in our STLC variant.
Fortunately, 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.
The definition that we gave above defines 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.
inductive Substi (s : Tm) (x : String) : Tm → Tm → Prop where
| var1 :
Substi s x (.var x) s
-- FILL IN HERE
theorem substi_correct (s : Tm) (x : String) (t t' : Tm) :
<{ [x := s] t }> = t' ↔ Substi s x t t' := s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] t }> = t' ↔ Substi s x t t'
All goals completed! 🐙
5.3.4. 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. t₁₂) v₂ ⟶ [x:=v₂] t₁₂
is traditionally called beta-reduction.
v.IsValue
----------------------- (appAbs)
(λx:T. t) v ⟶ [x:=v]t
t₁ ⟶ t₁'
---------------- (app1)
t₁ t₂ ⟶ t₁' t₂
v.IsValue
t₂ ⟶ t₂'
---------------- (app2)
v₁ t₂ ⟶ v₁ t₂'
... plus the usual rules for conditionals:
-------------------------------- (ifTrue)
(if true then t₁ else t₂) ⟶ t₁
--------------------------------- (ifFalse)
(if false then t₁ else t₂) ⟶ t₂
t₁ ⟶ t₁'
---------------------------------------------------- (ifStep)
(if t₁ then t₂ else t₃) ⟶ (if t₁' then t₂ else t₃)
This is call by value reduction: to reduce an
application (t₁ t₂), we
-
first reduce
t₁to a value: a functionλx:T. t -
then reduce the argument
t₂to a valuev -
then reduce the application itself by substituting
vfor the bound variablexin the bodyt.
Formally:
section
set_option hygiene false in
local notation:40 t:41 " ⟶ " t':41 => Step t t'
inductive Step : Tm → Tm → Prop where
| appAbs (x : String) (τ : Ty) (t v : Tm) (hv : v.IsValue) :
<{ (λ x : τ . t) v }> ⟶ <{ [x := v] t }>
| app1 (t₁ t₁' t₂ : Tm) (h : t₁ ⟶ t₁') :
<{ t₁ t₂ }> ⟶ <{ t₁' t₂ }>
| app2 (v₁ t₂ t₂' : Tm) (hv : v₁.IsValue) (h : t₂ ⟶ t₂') :
<{ v₁ t₂ }> ⟶ <{ v₁ t₂' }>
| ifTrue (t₁ t₂ : Tm) :
<{ if true then t₁ else t₂ }> ⟶ t₁
| ifFalse (t₁ t₂ : Tm) :
<{ if false then t₁ else t₂ }> ⟶ t₂
| ifStep (t₁ t₁' t₂ t₃ : Tm) (h : t₁ ⟶ t₁') :
<{ if t₁ then t₂ else t₃ }> ⟶ <{ if t₁' then t₂ else t₃ }>
end
scoped notation:40 t:41 " ⟶ " t':41 => Step t t'
scoped notation:40 t:41 " ⟶* " t':41 => Multi Step t t'
-- for later use with `normalize`
attribute [StlcEval] Step.appAbs Step.app1 Step.app2 Step.ifTrue Step.ifFalse Step.ifStep
As in the Smallstep chapter, ⟶* is the multi-step closure
of ⟶ — that is, Multi applied to this chapter's step relation. We
inherit its reflexivity lemma along with it, so a zero-step execution goal
t ⟶* t is closed by rfl.
What does the following term step to?
(λX:Bool → Bool. X) (λX:Bool. X) ⟶ ???
(A) λX:Bool. X
(B) λX:Bool → Bool. X
(C) (λX:Bool → Bool. X) (λX:Bool. X)
(D) none of the above
What does the following term step to?
(λX:Bool → Bool. X)
((λX:Bool → Bool. X) (λX:Bool. X))
⟶ ???
(A) λX:Bool. X
(B) λX:Bool → Bool. X
(C) (λX:Bool → Bool. X) (λX:Bool. X)
(D) (λX:Bool → Bool. X) ((λX:Bool → Bool. X) (λX:Bool. X))
(E) none of the above
What does the following term normalize to?
(λX:Bool → Bool. X) notB true ⟶* ???
where notB abbreviates λX:Bool. if X then false else true
(A) λX:Bool. X
(B) true
(C) false
(D) notB
(E) none of the above
What does the following term normalize to?
(λX:Bool. X) (notB true) ⟶* ???
(A) λX:Bool. X
(B) true
(C) false
(D) notB true
(E) none of the above
5.3.5. Examples
Example:
(λX:Bool → Bool. X) (λX:Bool. X) ⟶* λX:Bool. X
i.e.,
idBB idB ⟶* idB
example : <{ idBB idB }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB idB }> ⟶* idB
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB idB }> ⟶ idBx:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ idB ⟶* idB
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB idB }> ⟶ idB All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ idB ⟶* idB All goals completed! 🐙
Example:
(λX:Bool → Bool. X) ((λX:Bool → Bool. X) (λX:Bool. X))
⟶* λX:Bool. X
i.e.,
(idBB (idBB idB)) ⟶* idB.
example : <{ idBB (idBB idB) }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (idBB idB) }> ⟶* idB
-- the same reduction happens twice, so we name it
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶* idB
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶ <{ idBB idB }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶* idB
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶ <{ idBB idB }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶ idBx:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ idB ⟶* idB
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶ idB All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tystep₁:<{ idBB idB }> ⟶ idB⊢ idB ⟶* idB All goals completed! 🐙
Example:
(λX:Bool → Bool. X)
(λX:Bool. if X then false else true)
true
⟶* false
i.e.,
(idBB notB) true ⟶* false.
example : <{ idBB notB true }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB notB true }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB notB true }> ⟶ <{ notB true }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ notB true }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB notB true }> ⟶ <{ notB true }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ notB true }> ⟶ <{ if true then false else true }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ if true then false else true }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ notB true }> ⟶ <{ if true then false else true }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ if true then false else true }> ⟶ <{ false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ false }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ if true then false else true }> ⟶ <{ false }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ false }> ⟶* <{ false }> All goals completed! 🐙
Example:
(λX:Bool → Bool. X)
((λX:Bool. if X then false else true) true)
⟶* false
i.e.,
idBB (notB true) ⟶* false.
(Note that this term doesn't actually typecheck; even so, we can ask how it reduces.)
example : <{ idBB (notB true) }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (notB true) }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (notB true) }> ⟶ <{ idBB (if true then false else true) }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (if true then false else true) }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (notB true) }> ⟶ <{ idBB (if true then false else true) }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (if true then false else true) }> ⟶ <{ idBB false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB false }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (if true then false else true) }> ⟶ <{ idBB false }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB false }> ⟶ <{ false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ false }> ⟶* <{ false }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB false }> ⟶ <{ false }> All goals completed! 🐙
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ false }> ⟶* <{ false }> All goals completed! 🐙
As in the Smallstep chapter, we can use the normalize tactic to simplify
these proofs:
example : <{ idBB idB }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB idB }> ⟶* idB
All goals completed! 🐙
example : <{ idBB (idBB idB) }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (idBB idB) }> ⟶* idB
All goals completed! 🐙
example : <{ idBB notB true }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB notB true }> ⟶* <{ false }>
All goals completed! 🐙
example : <{ idBB (notB true) }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ idBB (notB true) }> ⟶* <{ false }>
All goals completed! 🐙
Do values and normal forms coincide in the language presented so far?
(A) yes
(B) no
5.4. Typing
Next we consider the typing relation of the STLC, which is meant to prevent reduction from getting stuck.
For instance, the following two STLC terms are both stuck:
-
if λX:Bool. X then true else false Here we branch on a function as though it were a boolean.
-
true false Here we apply a boolean as though it were a function.
5.4.1. Contexts
Although we are primarily interested in the binary relation
⊢ t ⦂ T, relating a closed term t to its type T, we need
to generalize a bit to make the definitions work.
Consider checking that λx:T₁₁. t₁₂ has type
T₁₁ → T₁₂. Intuitively, we need to check that t₁₂ has type
T₁₂. However, we have removed the binder λx, so x may occur
free in t₁₂ (that is, t₁₂ may be open). While checking that
t₁₂ has type T₁₂, we must remember that x has type T₁₁, in
order to deal with these free occurrences of x. Similarly, t₁₂
itself could contain abstractions, and typechecking their bodies
could require looking up the declared types of yet more free
variables.
To keep track of all this, we add a third element to the relation,
a typing context Γ, which records the types of the
variables that may occur free in a term — that is, Γ is a
partial map from variables to types.
The new typing judgment is written Γ ⊢ t ⦂ T and
informally read as "term t has type T, given the types of free
variables in t as specified by Γ".
We'll also write x ↦ T ; Γ for "update the partial map
Γ so that it maps x to T," following the notation from
the Typeclasses chapter.
With these refinements, we are ready to give informal and formal specifications of the typing relation.
abbrev Context := PartialMap String Ty
A context is a PartialMap from variable names to types — the partial
maps of the Typeclasses chapter, which are total maps whose values are
optional, so that none at a variable means "not bound here".
5.4.2. Typing Relation
Γ x = τ₁
------------ (var)
Γ ⊢ x ⦂ τ₁
x ↦ τ₂ ; Γ ⊢ t₁ ⦂ τ₁
------------------------- (abs)
Γ ⊢ λx:τ₂. t₁ ⦂ τ₂ → τ₁
Γ ⊢ t₁ ⦂ τ₂ → τ₁
Γ ⊢ t₂ ⦂ τ₂
------------------ (app)
Γ ⊢ t₁ t₂ ⦂ τ₁
----------------- (tru)
Γ ⊢ true ⦂ Bool
------------------ (fls)
Γ ⊢ false ⦂ Bool
Γ ⊢ t₁ ⦂ Bool Γ ⊢ t₂ ⦂ τ₁ Γ ⊢ t₃ ⦂ τ₁
--------------------------------------------- (ite)
Γ ⊢ if t₁ then t₂ else t₃ ⦂ τ₁
We can read the three-place relation Γ ⊢ t ⦂ T as:
"under the assumptions in Γ, the term t has the type T."
In the formal development, we write this judgment inside the same
<{ .. }> brackets we use for types and terms, as introduced by the
following notational conventions.
A context is written ∅ when empty and x ↦ τ ; Γ when extended with a
binding. The whole judgment then goes inside the same <{ … }> brackets as terms, written
with the turnstile and colon of the Types chapter:
<{ Γ ⊢ t ⦂ τ }>.
inductive HasType : Context → Tm → Ty → Prop where
| var (Γ : Context) (x : String) (τ₁ : Ty)
(h : Γ[x] = some τ₁) :
<{ Γ ⊢ ~(Tm.var x) ⦂ τ₁ }>
| abs (Γ : Context) (x : String)
(τ₁ τ₂ : Ty) (t₁ : Tm)
(h : <{ x ↦ τ₂ ; Γ ⊢ t₁ ⦂ τ₁ }>) :
<{ Γ ⊢ λ x : τ₂ . t₁ ⦂ τ₂ → τ₁ }>
| app (Γ : Context) (τ₁ τ₂ : Ty)
(t₁ t₂ : Tm)
(h₁ : <{ Γ ⊢ t₁ ⦂ τ₂ → τ₁ }>)
(h₂ : <{ Γ ⊢ t₂ ⦂ τ₂ }>) :
<{ Γ ⊢ t₁ t₂ ⦂ τ₁ }>
| tru (Γ : Context) :
<{ Γ ⊢ true ⦂ Bool }>
| fls (Γ : Context) :
<{ Γ ⊢ false ⦂ Bool }>
| ite (Γ : Context) (t₁ t₂ t₃ : Tm) (τ₁ : Ty)
(h₁ : <{ Γ ⊢ t₁ ⦂ Bool }>)
(h₂ : <{ Γ ⊢ t₂ ⦂ τ₁ }>)
(h₃ : <{ Γ ⊢ t₃ ⦂ τ₁ }>) :
<{ Γ ⊢ if t₁ then t₂ else t₃ ⦂ τ₁ }>
attribute [StlcTyping] HasType.var HasType.abs HasType.app HasType.tru HasType.fls HasType.ite
Notation encoding
open Lean PrettyPrinter in
@[app_unexpander HasType]
def HasType.unexpand : Unexpander := StlcCommon.Delab.unexpandHasType
5.4.3. Examples
example : <{ ∅ ⊢ λ X : Bool . X ⦂ Bool → Bool }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ ∅ ⊢ λ X : Bool . X ⦂ Bool → Bool }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ X ↦ Bool ; ∅ ⊢ X ⦂ Bool }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ ("X" →ₚ <{ Bool }>)["X"] = some (<{ Bool }>); All goals completed! 🐙
The derivation is small enough to write out directly: an abstraction rule
whose premise is the variable rule, and the variable rule's premise — that the
extended context maps X to Bool — holds by computation, hence rfl.
Much like reduction sequences, long derivations of typing rules can grow
quite tedious to prove. Luckily, we can have Lean automate proofs of this sort,
using another tactic: apply_rules. This tactic works much like
normalize, but is more efficient and will make progress even if it cannot
solve the goal outright. Like normalize, apply_rules also
takes a using argument which tells Lean which set of constructors to draw from.
∅ ⊢ λX:Bool. λY:Bool → Bool. Y (Y X)
⦂ Bool → (Bool → Bool) → Bool.
example :
<{ ∅ ⊢ λ X : Bool . λ Y : Bool → Bool . Y (Y X) ⦂
Bool → (Bool → Bool) → Bool }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ ∅ ⊢ λ X : Bool . λ Y : Bool → Bool . Y (Y X) ⦂ Bool → (Bool → Bool) → Bool }>
All goals completed! 🐙
It's worth noting that apply_rules relies on an important property
of our typing rules - namely, that they are syntax directed. A syntax directed
judgment is one where the syntax of a term completely determines which rule
can be applied at any given time; only one rule can be applied to each term.
This is important because apply_rules just applies the first rule in
its set of constructors or lemmas that it can - it doesn't backtrack if that rule
isn't correct. So, making sure that only one rule can apply to any given term
is important to ensure that apply_rules always discovers a valid
derivation, if one exists.
Prove the same result, applying one rule at a time and naming the argument type of each application explicitly.
example :
<{ ∅ ⊢ λ X : Bool . λ Y : Bool → Bool . Y (Y X) ⦂
Bool → (Bool → Bool) → Bool }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ <{ ∅ ⊢ λ X : Bool . λ Y : Bool → Bool . Y (Y X) ⦂ Bool → (Bool → Bool) → Bool }>
All goals completed! 🐙
Formally prove the following typing derivation holds:
∃ τ,
∅ ⊢ λX : Bool → Bool . λY : Bool → Bool . λZ : Bool .
Y (X Z)
⦂ τ
example :
∃ τ, <{ ∅ ⊢ λ X : Bool → Bool . λ Y : Bool → Bool . λ Z : Bool . Y (X Z)
⦂ τ }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ ∃ τ, <{ ∅ ⊢ λ X : Bool → Bool . λ Y : Bool → Bool . λ Z : Bool . Y (X Z) ⦂ τ }>
All goals completed! 🐙
We can also show that some terms are not typable. For example,
we can check that there is no typing derivation assigning a type
to the term λX:Bool. λY:Bool. X Y — i.e.,
¬ ∃ τ, ∅ ⊢ λX:Bool. λY:Bool. X Y ⦂ τ
example : ¬ ∃ τ, <{ ∅ ⊢ λ X : Bool . λ Y : Bool . X Y ⦂ τ }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ ¬∃ τ, <{ ∅ ⊢ λ X : Bool . λ Y : Bool . X Y ⦂ τ }>
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ✝:Tyτ:Tyhc:<{ ∅ ⊢ λ X : Bool . λ Y : Bool . X Y ⦂ τ }>⊢ False
-- Each `cases` peels off one rule of the derivation, naming the premise it
-- leaves behind; the context stays small because the old hypothesis goes away.
cases hc with
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tyτ₁✝:Tyh₁:<{ X ↦ Bool ; ∅ ⊢ λ Y : Bool . X Y ⦂ τ₁✝ }>⊢ False
cases h₁ with
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tyτ₁✝:Tyh₂:<{ Y ↦ Bool ; X ↦ Bool ; ∅ ⊢ X Y ⦂ τ₁✝ }>⊢ False
cases h₂ with
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tyτ₁✝:Tyτ₂✝:Tyh₂✝:<{ Y ↦ Bool ; X ↦ Bool ; ∅ ⊢ Y ⦂ τ₂✝ }>hf:<{ Y ↦ Bool ; X ↦ Bool ; ∅ ⊢ X ⦂ τ₂✝ → τ₁✝ }>⊢ False
cases hf with
x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Tyτ₁✝:Tyτ₂✝:Tyh₂✝:<{ Y ↦ Bool ; X ↦ Bool ; ∅ ⊢ Y ⦂ τ₂✝ }>hx:("Y" →ₚ <{ Bool }> ; "X" →ₚ <{ Bool }>)["X"] = some (<{ τ₂✝ → τ₁✝ }>)⊢ False
-- `X` is bound to `Bool` in the context, but the application rule
-- needs it to have an arrow type.
All goals completed! 🐙
Another nonexample:
¬ ∃ σ τ, ∅ ⊢ λX:σ. X X ⦂ τ
example : ¬ ∃ τ σ, <{ ∅ ⊢ λ X : τ . X X ⦂ σ }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:Tmτ:Ty⊢ ¬∃ τ σ, <{ ∅ ⊢ λ X : τ . X X ⦂ σ }>
All goals completed! 🐙
Which of the following propositions is not provable?
(A) Y ↦ Bool ; ∅ ⊢ λX:Bool. X ⦂ Bool → Bool
(B) ∃ τ, ∅ ⊢ λY:Bool → Bool. λX:Bool. Y X ⦂ τ
(C) ∃ τ, ∅ ⊢ λY:Bool → Bool. λX:Bool. X Y ⦂ τ
(D) ∃ σ, X ↦ σ ; ∅ ⊢ λY:Bool → Bool. Y X ⦂ (Bool → Bool) → σ
Which of these is not provable?
(A) ∃ τ, ∅ ⊢ λY:Bool → Bool → Bool. λX:Bool. Y X ⦂ τ
(B) ∃ σ τ, X ↦ σ ; ∅ ⊢ X X X ⦂ τ
(C) ∃ σ υ τ, X ↦ σ ; Y ↦ υ ; ∅ ⊢ λZ:Bool. X (Y Z) ⦂ τ
(D) ∃ σ τ, X ↦ σ ; ∅ ⊢ λY:Bool. X (X Y) ⦂ τ
end Stlc