module ProgrammingLanguage.STLC.Simple where
The simply-typed lambda calculus🔗
The simple-typed lamdba calclus (STLC) is an example of one of the smallest “useful” typed programming languages. While very simple, and lacking features that would make it useful for real programming, its small size makes it very appealing for the demonstration of programming language formalization.
This file represents the first in a series, exploring several different approaches to formalizing various properties of the STLC. We will start with the most “naive” approach, and build off it to more advanced approaches.
First, the types of the STLC: the Unit type, products, and functions.
data Ty : Type where
: Ty
UU _`×_ : Ty → Ty → Ty
_`⇒_ : Ty → Ty → Ty
We model contexts as (snoc) lists of pairs, alongside a partial index
function. We use ∷c
as the
“reversed” list constructor to avoid confusion about insertion order.
Sequels to this file will use more advanced techniques to avoid this
partiality, but it’s alright for right now.
: Type
Con = List (Nat × Ty) Con
infixl 10 _∷c_
pattern _∷c_ Γ x = x ∷ Γ
: Con → Nat → Maybe Ty
index _ = nothing
index [] (Γ ∷c (n , ty)) k with k ≡? n
index ... | yes _ = just ty
... | no _ = index Γ k
We also note some minor lemmas around indexing:
: ∀ {Γ n t} → index (Γ ∷c (n , t)) n ≡ just t
index-immediate : ∀ {Γ n k t₁ t₂ ρ} →
index-duplicate (Γ ∷c (n , t₁)) k ≡ just ρ →
index ((Γ ∷c (n , t₂)) ∷c (n , t₁)) k ≡ just ρ index
{Γ} {n} {t} with n ≡? n
index-immediate ... | yes n≡n = refl
... | no ¬n≡n = absurd (¬n≡n refl)
{Γ} {n} {k} {t₁} {t₂} {ρ} eq with k ≡? n
index-duplicate ... | yes k≡n = eq
... | no ¬k≡n with k ≡? n
... | yes k≡n = absurd (¬k≡n k≡n)
... | no ¬k≡n = eq
Then, expressions: we have variables, functions and application, pairs and projections, and the unit.
data Expr : Type where
: Nat → Expr
` : Nat → Expr → Expr
`λ : Expr → Expr → Expr
`$ _,_⟩ : Expr → Expr → Expr
`⟨: Expr → Expr
`π₁ : Expr → Expr
`π₂ : Expr `U
Some application lemmas, for convinience.
: ∀ {a b x} → a ≡ b → `$ a x ≡ `$ b x
`$-apₗ : ∀ {f a b} → a ≡ b → `$ f a ≡ `$ f b
`$-apᵣ
{x = x} a≡b = ap (λ k → `$ k x) a≡b
`$-apₗ {f = f} a≡b = ap (λ k → `$ f k) a≡b `$-apᵣ
We must then define a relation to assign types to expressions, which
we will notate Γ ⊢ tm ⦂ ty
, for “a
term
has type
in the context
infix 3 _⊢_⦂_
data _⊢_⦂_ : Con → Expr → Ty → Type where
We say that a variable
has a type
in context
if index Γ n ≡ just τ
.
: ∀ {Γ τ} (n : Nat) →
`⊢ →
index Γ n ≡ just τ Γ ⊢ ` n ⦂ τ
For lambda abstraction, if an expression extended with a variable of type has type we say that has type
: ∀ {Γ n body τ ρ} →
`λ⊢ (n , τ) ⊢ body ⦂ ρ →
Γ ∷c Γ ⊢ `λ n body ⦂ τ `⇒ ρ
If an expression has type and an expression has type then the application (written here as has type
: ∀ {Γ f x τ ρ} →
`·⊢ →
Γ ⊢ f ⦂ τ `⇒ ρ →
Γ ⊢ x ⦂ τ Γ ⊢ `$ f x ⦂ ρ
The rest of the formers follow these patterns:
: ∀ {Γ a b τ ρ} →
`⟨,⟩⊢ →
Γ ⊢ a ⦂ τ →
Γ ⊢ b ⦂ ρ
Γ ⊢ `⟨ a , b ⟩ ⦂ τ `× ρ
: ∀ {Γ a τ ρ} →
`π₁⊢ →
Γ ⊢ a ⦂ τ `× ρ
Γ ⊢ `π₁ a ⦂ τ
: ∀ {Γ a τ ρ} →
`π₂⊢ →
Γ ⊢ a ⦂ τ `× ρ
Γ ⊢ `π₂ a ⦂ ρ
: ∀ {Γ} →
`U⊢ Γ ⊢ `U ⦂ UU
This completes our typing relation. We can now show that some given program has some given type, for example:
module Example-1 where
: Expr
const = `λ 0 (`λ 1 (` 0))
const
: [] ⊢ const ⦂ UU `⇒ (UU `⇒ UU)
const-is-UU⇒UU⇒UU = `λ⊢ (`λ⊢ (`⊢ 0 refl)) const-is-UU⇒UU⇒UU
The astute amongst you may note that the typing derivation looks suspiciously similar to the term itself - this will be explored later in the series.
Now we will take a slight detour, and define what it means for an expression to be a value. This will come in useful in a second! For right now, we note that a value is something that cannot be reduced further - in our case, variables, lambda abstractions, pairs, and unit.
data Value : Expr → Type where
: ∀ n → Value (` n)
v-var : ∀ n body → Value (`λ n body)
v-λ : ∀ a b → Value (`⟨ a , b ⟩)
v-⟨,⟩ : Value `U v-U
Our next goal is to now define a “step” relation, which dictates that a term may, through a reduction, step to another expression that represents one “step” of evaluation.
This is how we will define the evaluation of our expressions. Before
we can define stepping, we need to define substitution, so that we may
turn an expression like
into
We notate the substitution of a variable
for an expression
in another expression
as f [ n := e ]
. The method of
substitution we implement is called capture-avoiding
substitution.
infix 2 _[_:=_]
_[_:=_] : Expr → Nat → Expr → Expr
If a variable x is equal to the variable we are substituing for, n, we return the new expression. Else, the variable unchanged.
with x ≡? n
` x [ n := e ] ... | yes _ = e
... | no _ = ` x
Here is why it’s called capture-avoiding: if our lambda binds the
variable name again, we don’t substitute inside. In other words, the
substituion (λ y. y) y [y := k]
yields (λ y. y) k
,
not (λ y. k) k
.
with x ≡? n
`λ x f [ n := e ] ... | yes _ = `λ x f
... | no _ = `λ x (f [ n := e ])
In all other cases, we simply “move” the substition into all subexpressions. (Or, do nothing.)
= `$ (f [ n := e ]) (x [ n := e ])
`$ f x [ n := e ] = `⟨ a [ n := e ] , b [ n := e ] ⟩
`⟨ a , b ⟩ [ n := e ] = `π₁ (a [ n := e ])
`π₁ a [ n := e ] = `π₂ (a [ n := e ])
`π₂ a [ n := e ] = `U `U [ n := e ]
Now, we define our step relation proper.
data Step : Expr → Expr → Type where
The act of turning an application into is called β-reduction for lambda terms. We require to be a value in order to keep reduction deterministic – this will be elaborated on in a moment.
: ∀ {n body x} →
β-λ →
Value x (`$ (`λ n body) x) (body [ n := x ]) Step
Likewise, reducing projections on a pair is called β-reduction for pairs.
: ∀ {a b} →
β-π₁ (`π₁ `⟨ a , b ⟩) a
Step : ∀ {a b} →
β-π₂ (`π₂ `⟨ a , b ⟩) b Step
We also have two reductions that can step “inside” projections, which we will call ξ rules.
: ∀ {a₁ a₂} →
ξ-π₁ →
Step a₁ a₂ (`π₁ a₁) (`π₁ a₂)
Step
: ∀ {a₁ a₂} →
ξ-π₂ →
Step a₁ a₂ (`π₂ a₁) (`π₂ a₂) Step
Likewise, we have one that can step inside an application, on the left hand side.
: ∀ {f₁ f₂ x} →
ξ-$ₗ →
Step f₁ f₂ (`$ f₁ x) (`$ f₂ x) Step
We also include a rule for stepping on the right hand side, requiring
the left to be a value first. This, combined with the value requirement
of the β-λ
rule, keep our
evaluation deterministic, forcing that evaluation
should take place from left to right. We will prove this later.
: ∀ {f x₁ x₂} →
ξ-$ᵣ →
Value f →
Step x₁ x₂ (`$ f x₁) (`$ f x₂) Step
These are all of our step rules! The STLC is indeed very simple. We can now show that, say, an identity function applied to something reduces properly:
module Example-2 where
: Expr
our-id = `λ 0 (` 0)
our-id
: Expr
pair = `⟨ `U , `U ⟩
pair
: Expr
id-app = `$ our-id pair
id-app
: Step id-app pair
id-app-step = β-λ (v-⟨,⟩ `U `U) id-app-step
TODO: Refl Trans closure of Step
The big two properties🔗
The two “big” properties about the STLC we wish to prove are called progress and preservation. Progress states that any given term is either done (a value), or can take another step. Preservation states that if a well typed expression steps to another they have the same type (i.e., stepping preserves type.)
The first step in proving these is showing that a “proper” substituion preserves types. If a term has type when extended with a variable of type then substituting any expression of type for preserves the type of To prove this, we first show that renaming preserves types - if and are contexts, and for every index in gives the same type, then any term with a type under has the same type under
: ∀ {Γ Δ} →
rename (∀ n ty → index Γ n ≡ just ty → index Δ n ≡ just ty) →
(∀ tm ty → Γ ⊢ tm ⦂ ty → Δ ⊢ tm ⦂ ty)
Variables are fairly straightforward - we simply apply our renaming function.
{Γ} {Δ} f (` x) ty (`⊢ .x n) = `⊢ x (f x ty n) rename
Lambda abstractions are more complex - we need to extend our renaming function to encompass the new abstraction.
{Γ} {Δ} f (`λ x tm) ty (`λ⊢ {τ = τ} {ρ = ρ} Γ⊢) = `λ⊢ (rename f' tm ρ Γ⊢)
rename where
: (n : Nat) (ty : Ty) →
f' (Γ ∷c (x , τ)) n ≡ just ty →
index (Δ ∷c (x , τ)) n ≡ just ty
index with n ≡? x
f' n ty Γ≡ ... | yes x≡n = Γ≡
... | no p = f n ty Γ≡
Everything else is straightforward, as in the substitution case.
{Γ} {Δ} f (`$ f' x) ty (`·⊢ {τ = τ} Γ⊢₁ Γ⊢₂) =
rename (rename f f' (τ `⇒ ty) Γ⊢₁) (rename f x τ Γ⊢₂)
`·⊢
{Γ} {Δ} f `⟨ a , b ⟩ ty (`⟨,⟩⊢ {τ = τ} {ρ = ρ} Γ⊢₁ Γ⊢₂) =
rename (rename f a τ Γ⊢₁) (rename f b ρ Γ⊢₂)
`⟨,⟩⊢
{Γ} {Δ} f (`π₁ tm) ty (`π₁⊢ {ρ = ρ} Γ⊢) = `π₁⊢ (rename f tm (ty `× ρ) Γ⊢)
rename {Γ} {Δ} f (`π₂ tm) ty (`π₂⊢ {τ = τ} Γ⊢) = `π₂⊢ (rename f tm (τ `× ty) Γ⊢)
rename {Γ} {Δ} f `U ty `U⊢ = `U⊢ rename
Another few lemmas! This time about shuffling and dropping names in the context.
: ∀ {Γ n t₁ t₂ bd typ} →
duplicates-are-ok (n , t₂) ∷c (n , t₁) ⊢ bd ⦂ typ →
Γ ∷c (n , t₁) ⊢ bd ⦂ typ
Γ ∷c : ∀ {Γ n k t₁ t₂ bd typ} →
variable-swap →
¬ n ≡ k (n , t₁) ∷c (k , t₂) ⊢ bd ⦂ typ →
Γ ∷c (k , t₂) ∷c (n , t₁) ⊢ bd ⦂ typ
Γ ∷c
{Γ} {n} {k} {t₁} {t₂} {x} {typ} ¬n≡k Γ⊢ = rename f x typ Γ⊢
variable-swap where
: (z : Nat) (ty : Ty) →
f (Γ ∷c (n , t₁) ∷c (k , t₂)) z ≡ just ty →
index (Γ ∷c (k , t₂) ∷c (n , t₁)) z ≡ just ty
index with z ≡? n in eq
f z ty x ... | no ¬z≡n = h
where
: (index (Γ ∷c (k , t₂)) z) ≡ just ty
h with z ≡? k
h ... | yes z≡k = x
... | no ¬z≡k with z ≡? n
... | no ¬z≡n = x
... | yes z≡n with z ≡? k
... | yes z≡k = absurd (¬n≡k (sym z≡n ∙ z≡k))
... | no ¬z≡k with z ≡? n
... | yes z≡n = x
... | no ¬z≡n = absurd (¬z≡n z≡n)
{Γ} {n} {t₁} {t₂} {bd} {typ} Γ⊢ =
duplicates-are-ok
rename f bd typ Γ⊢where
: (k : Nat) (ty : Ty) →
f (Γ ∷c (n , t₂) ∷c (n , t₁)) k ≡ just ty →
index (Γ ∷c (n , t₁)) k ≡ just ty
index with k ≡? n
f k ty x ... | yes k≡n = x
... | no ¬k≡n with k ≡? n
... | yes k≡n = absurd (¬k≡n k≡n)
... | no ¬k≡n = x
We need one additional important lemma - weaking. It says that if a
term has a type in the empty context, it also has that type in any other
context. This turns out to be a special case of renaming, where we get
an absurdity from considering that index [] n ≡ just τ
, for any
and
: ∀ {Γ tm ty} →
weakening →
[] ⊢ tm ⦂ ty
Γ ⊢ tm ⦂ ty{Γ} {tm} {ty} []⊢ = rename f tm ty []⊢
weakening where
: (n : Nat) (τ : Ty) → index [] n ≡ just τ → index Γ n ≡ just τ
f _ _ x = absurd (nothing≠just x) f
Now with renaming under our belt, we can prove substitution proper preserves types. Note that the substitute’s type must exist in the empty context, to prevent conflicts of variables.
: ∀ {Γ n t bd typ s} →
subst-pres →
[] ⊢ s ⦂ t (n , t) ⊢ bd ⦂ typ →
Γ ∷c Γ ⊢ bd [ n := s ] ⦂ typ
In the case of variables, we use weakening for the substituion
itself, to embed our term s
into
the context
{Γ} {n} {t} {` x} {typ} {s} s⊢ (`⊢ .x k) with x ≡? n
subst-pres ... | yes _ = weakening (subst (λ ρ → [] ⊢ s ⦂ ρ) (just-inj k) s⊢)
... | no _ = `⊢ x k
Lambda abstraction is once again slightly annoying. Handling the case where the names are equal requires some removing of duplicates in the context, and where they are not equal requires some shuffling.
{Γ} {n} {t} {`λ x bd} {typ} {s} s⊢ (`λ⊢ {τ = τ} {ρ = ρ} Γ⊢) with x ≡? n
subst-pres ... | yes x≡n = `λ⊢ (duplicates-are-ok
(subst (λ _ → Γ ∷c _ ∷c _ ⊢ bd ⦂ ρ) (sym x≡n) Γ⊢))
... | no ¬x≡n = `λ⊢ (subst-pres s⊢ (variable-swap (λ x≡n → ¬x≡n (sym x≡n)) Γ⊢))
The rest proceeds nicely.
{Γ} {n} {t} {`$ bd bd₁} {typ} {s} s⊢ (`·⊢ Γ⊢₁ Γ⊢₂) =
subst-pres (subst-pres s⊢ Γ⊢₁) (subst-pres s⊢ Γ⊢₂)
`·⊢
{Γ} {n} {t} {`⟨ a , b ⟩} {typ} {s} s⊢ (`⟨,⟩⊢ Γ⊢₁ Γ⊢₂) =
subst-pres (subst-pres s⊢ Γ⊢₁) (subst-pres s⊢ Γ⊢₂)
`⟨,⟩⊢
{Γ} {n} {t} {`π₁ bd} {typ} {s} s⊢ (`π₁⊢ Γ⊢) = `π₁⊢ (subst-pres s⊢ Γ⊢)
subst-pres {Γ} {n} {t} {`π₂ bd} {typ} {s} s⊢ (`π₂⊢ Γ⊢) = `π₂⊢ (subst-pres s⊢ Γ⊢)
subst-pres {Γ} {n} {t} {`U} {typ} {s} s⊢ `U⊢ = `U⊢ subst-pres
We’ll do preservation first, which follows very easily from the lemmas we’ve already defined:
: ∀ {x₁ x₂ typ} →
preservation →
Step x₁ x₂ →
[] ⊢ x₁ ⦂ typ
[] ⊢ x₂ ⦂ typ
(β-λ p) (`·⊢ (`λ⊢ ⊢f) ⊢x) = subst-pres ⊢x ⊢f
preservation (`π₁⊢ (`⟨,⟩⊢ ⊢a ⊢b)) = ⊢a
preservation β-π₁ (`π₂⊢ (`⟨,⟩⊢ ⊢a ⊢b)) = ⊢b
preservation β-π₂ (ξ-π₁ step) (`π₁⊢ ⊢a) = `π₁⊢ (preservation step ⊢a)
preservation (ξ-π₂ step) (`π₂⊢ ⊢b) = `π₂⊢ (preservation step ⊢b)
preservation (ξ-$ₗ step) (`·⊢ ⊢f ⊢x) = `·⊢ (preservation step ⊢f) ⊢x
preservation (ξ-$ᵣ val step) (`·⊢ ⊢f ⊢x) = `·⊢ ⊢f (preservation step ⊢x) preservation
Then, progress, noting that the expression must be well typed. We define progress as a datatype, as it’s much nicer to work with.
data Progress (M : Expr): Type where
: ∀ {N} →
going →
Step M N
Progress M: Value M → Progress M done
Then, progress reduces to mostly a lot of case analysis.
: ∀ {x ty} →
progress →
[] ⊢ x ⦂ ty
Progress x
(`⊢ n n∈) = absurd (nothing≠just n∈)
progress (`λ⊢ {n = n} {body = body} ⊢x) = done (v-λ n body)
progress (`·⊢ ⊢f ⊢x) with progress ⊢f
progress ... | going next-f = going (ξ-$ₗ next-f)
... | done vf with progress ⊢x
... | going next-x = going (ξ-$ᵣ vf next-x)
... | done vx with ⊢f
... | `⊢ n n∈ = absurd (nothing≠just n∈)
... | `λ⊢ f = going (β-λ vx)
(`⟨,⟩⊢ {a = a} {b = b} ⊢a ⊢b) = done (v-⟨,⟩ a b)
progress (`π₁⊢ {a = a} ⊢x) with progress ⊢x
progress ... | going next = going (ξ-π₁ next)
... | done (v-⟨,⟩ a b) = going β-π₁
... | done (v-var n) with ⊢x
... | `⊢ _ x∈ = absurd (nothing≠just x∈)
(`π₂⊢ ⊢x) with progress ⊢x
progress ... | going next = going (ξ-π₂ next)
... | done (v-⟨,⟩ a b) = going β-π₂
... | done (v-var n) with ⊢x
... | `⊢ _ x∈ = absurd (nothing≠just x∈)
= done v-U progress `U⊢
There’s our big two properties! As promised, we’ll also now prove that our step relation is deterministic – there is only one step that can be applied at any given time. This is also equivalent to saying that if some term steps to and also to then
We do this with the help of a lemma that states values do not step to anything.
: ∀ {x y} →
value-¬step →
Value x (Step x y) ¬
(v-var n) ()
value-¬step (v-λ n body) ()
value-¬step (v-⟨,⟩ a b) ()
value-¬step () value-¬step v-U
: ∀ {x ty x₁ x₂} →
deterministic →
[] ⊢ x ⦂ ty →
Step x x₁ →
Step x x₂
x₁ ≡ x₂
(`·⊢ ⊢f ⊢x) (β-λ vx₁) (β-λ vx₂) = refl
deterministic (`·⊢ ⊢f ⊢x) (β-λ vx) (ξ-$ᵣ x b) = absurd (value-¬step vx b)
deterministic (`·⊢ ⊢f ⊢x) (ξ-$ₗ →x₁) (ξ-$ₗ →x₂) =
deterministic (deterministic ⊢f →x₁ →x₂)
`$-apₗ
(`·⊢ ⊢f ⊢x) (ξ-$ₗ →x₁) (ξ-$ᵣ vx →x₂) = absurd (value-¬step vx →x₁)
deterministic (`·⊢ ⊢f ⊢x) (ξ-$ᵣ vx₁ →x₁) (β-λ vx₂) = absurd (value-¬step vx₂ →x₁)
deterministic (`·⊢ ⊢f ⊢x) (ξ-$ᵣ vx →x₁) (ξ-$ₗ →x₂) = absurd (value-¬step vx →x₂)
deterministic (`·⊢ ⊢f ⊢x) (ξ-$ᵣ _ →x₁) (ξ-$ᵣ _ →x₂) =
deterministic (deterministic ⊢x →x₁ →x₂)
`$-apᵣ
(`π₁⊢ ⊢x) β-π₁ β-π₁ = refl
deterministic (`π₁⊢ ⊢x) (ξ-π₁ →x₁) (ξ-π₁ →x₂) = ap `π₁ (deterministic ⊢x →x₁ →x₂)
deterministic (`π₂⊢ ⊢x) β-π₂ β-π₂ = refl
deterministic (`π₂⊢ ⊢x) (ξ-π₂ →x₁) (ξ-π₂ →x₂) = ap `π₂ (deterministic ⊢x →x₁ →x₂) deterministic