Notes on Lean

Functions🔗

We consider a sublanguage that is a pure type system. A pure type system is defined by its universes, the relations between the universes, and a number of rules. The rules govern

  • formation: how a type is created

  • introduction: how expressions of a type are created

  • elimination: how expressions of a type are transformed to expressions of another type

  • reduction: how introduction and elimination interact

In the case of Lean, a further category is useful:

  • equality: which expressions of a type are equal

These organizational categories are applied here to functions. We later apply them to inductive types and quotient types.

Preliminaries🔗

To understand the formation rule for function types, we must first consider the universes and the relations between them. We introduce also proof irrelevance, since it lies behind an exceptional case of the rule.

Proof irrelevance🔗

If a has type α, we say that a inhabits α and that α is inhabited. Recall that Prop is the universe of propositions. Each expression inhabiting Prop is a type encoding a proposition, and proving a proposition amounts to giving an expression inhabiting the proposition. We call an expression inhabiting a proposition a proof.

Definitional equality includes proof irrelevance: any two proofs of the same proposition are equal.

example (p : Prop) (pf₁ pf₂ : p) : pf₁ = pf₂ := rfl

Universes🔗

The universe of propositions Prop inhabits the type universe Type.

example : Type := Prop

Type itself inhabits a type.

example : Type 1 := Type

In fact, there is an infinite sequence of type universes,

example : Type 2 := Type 1
example : Type 3 := Type 2

and so on. Type is an abbreviation for Type 0.

example : Type = Type 0 := rfl

The infinite sequence Prop, Type 0, Type 1, … is syntactic sugar for the universe hierarchy Sort 0, Sort 1, Sort 2, …. Here Sort u is called a universe and u is its level. We can verify that the two sequences coincide using rfl.

example : Prop = Sort 0 := rfl
example : Type u = Sort (u + 1) := rfl

Relations between universes🔗

A pure type system comes with a relation on its universes specifying which universes inhabit each other. In the case of Lean this relation is fully described by

example : Sort (u + 1) := Sort u

Each universe inhabits the next one and no others.

More generally, each type α inhabits Sort u for exactly one u. We say that this Sort u is the universe of α.

Formation🔗

An elementary function type is formed as follows.

example (α β : Type) : Type := α → β

Here the types α and β specify the domain and codomain, respectively. The operator → is right-associative, that is,

example (α β γ : Type) : (α → β → γ) = (α → (β → γ)) := rfl

This is often viewed as encoding functions that take two arguments, the first in α and the second in β, and yield a result in γ. We refer to γ as the final codomain. Moreover, we say that the final codomain of a type δ is δ itself whenever δ is not a function type.[…]This unified terminology across all types is used for instance when discussing type constructors and well-formedness requirements of inductive types.

Recall that the codomain of a function may depend on its argument. Consider the following abstract example.

example (I : Type) (X : I → Type) : Type := (i : I) → X i

We refer to (i : I) → X i as a \Pi-type.[…]\Pi-types are also called dependent function types. Such a type can be thought of as encoding an indexed product of sets, \prod_{i \in I} X_i = \left\{\left. f: I \to \bigcup_{i \in I} X_i\ \right| \ f(i) \in X_i,\ i \in I \right\}.

In fact, all function types are \Pi-types. In the elementary case discussed above, the codomain simply does not depend on the argument.

example
  (α : Sort u) (β : Sort v) : (α → β) = ((a : α) → β)
:= rfl

Since a does not appear in the codomain, we can rewrite the type leaving it as a hole.

example
  (α : Sort u) (β : Sort v) : (α → β) = ((_ : α) → β)
:= rfl

Impredicative maximum rule🔗

The formation of a \Pi-type is governed by the following impredicative maximum rule.[…]This name is not used in the Lean Language Reference; the rule itself is described in Predicativity. The level expression imax u v is called the impredicative maximum (or least upper bound) of u and v. We have named the rule accordingly.

def imax_rule (I : Sort u) (X : I → Sort v) :
  Sort (imax u v) := (i : I) → X i

where

example : Sort (imax _ 0) = Sort 0 := rfl

example : Sort (imax u (v + 1)) = Sort (max u (v + 1))
:= rfl

Consider the implication.

example (p : Prop) (q : Prop) : Prop := p → q

The type Prop of p → q arises from the impredicative maximum rule. Indeed, since

example (p : Prop) : Sort 0 := p
example (q : Prop) : Sort 0 := q

the rule applies with u = 0 and v = 0, yielding the type

example : Sort (imax 0 0) = Prop := rfl

Let us now consider universal quantification.

example
  (α : Sort u) (P : α → Prop)
  : (∀ a : α, P a) = ((a : α) → P a)
:= rfl

The type of predicates on α satisfies

example (α : Sort u) : Sort (max u 1) := α → Prop

Here Sort (max u 1) arises from the impredicative maximum rule. Indeed, since

example : Sort 1 := Prop

the rule applies with v = 1, yielding Sort (max u 1). The universal quantification, on the other hand, is a proposition.

example (α : Sort u) (P : α → Prop) : Prop := (a : α) → P a

Here Prop arises from the impredicative maximum rule. Indeed, since the evaluation P a of a predicate is a proposition,

example (α : Sort u) (P : α → Prop) (a : α) : Sort 0 := P a

the rule applies with v = 0 yielding

example : Prop = Sort (imax _ 0) := rfl

Implicit arguments🔗

We have used extensively rfl. It is a function taking two implicit arguments. Implicit arguments are written using curly braces.

example : {α : Sort u} → {a : α} → a = a := rfl

The first argument is a type α, and the second is an expression a of that type. The final codomain a = a depends on the arguments. The implicit arguments can be inferred from it due to this dependence. In general, implicit arguments are translated into their explicit counterparts during elaboration.

Prefixing a function with @ makes all its implicit arguments explicit.

example : (α : Sort u) → (a : α) → a = a := @rfl

example (α : Sort u) (a : α) : a = a := @rfl α a

Universe polymorphism🔗

The function rfl is universe-polymorphic: it depends on a universe level, denoted by u in the following example.

example : (α : Sort u) → (a : α) → a = a := @rfl

A universe-polymorphic function can be instantiated at fixed universe levels.

example : (α : Type) → (a : α) → a = a := @rfl

example : (α : Prop) → (a : α) → a = a := @rfl

If a name depends on universe levels, diagnostic output shows them in curly braces after the name, with a dot in between.

rfl.{u} {α : Sort u} {a : α} : a = a#check rfl

Girard's paradox🔗

The impredicative maximum rule relies on the infinite sequence of universes as seen by considering the function type with the same universe as domain and codomain.

def pi : Type (w + 1) := Type w → Type w

Having an infinite number of universes is not a feature introduced by choice, rather it is the price of consistency. Certain historical pure type systems that lack sufficient stratification are inconsistent.

In Lean, violating the impredicative maximum rule would lead to Girard's paradox, formulated as follows.

example
  (π : (Type w → Type w) → Type w)
  (Λ : {ξ : Type w → Type w} → ((i : Type w) → ξ i) → π ξ)
  (app : {ξ : Type w → Type w} → π ξ → (i : Type w) → ξ i)
  (β : ∀
    {ξ : Type w → Type w}
    (f : (i : Type w) → ξ i)
    (x : Type w),
    app (Λ f) x = f x
  )
  : 1 = 0
:= False.elim (Counterexample.girard π Λ app β)

The paradox assumes a formation rule π, incompatible with the above special case pi of the impredicative maximum rule. The codomain of π is one level below the universe of pi.

In the paradox, Λ, app, and β model \lambda-abstraction, function application, and \beta-reduction, respectively. These concepts are described below. The flawed formation rule π, together with Λ, app, and β, leads to the contradiction 1 = 0.

The special case of imax

example : Sort (imax _ 0) = Prop := rfl

is related to proof irrelevance. Heuristically speaking, since proofs carry no information beyond the fact that a proposition holds, they do not enable the kind of self-referential constructions that lead to paradoxes.

Introduction🔗

Functions are introduced by \lambda-abstraction. As an illustration, we consider the identity function.

def I₁ {α : Sort u} : α → α := λ x ↦ x

Here are some syntactic variations of the same function.

def I₂ {α : Sort u} := λ x : α ↦ x

def I₃ {α : Sort u} := λ (x : α) ↦ x

def I₄ {α : Sort u} (x : α) := x

variable {α : Sort u} in
def I₅ (x : α) := x

def I₆ {α : Sort u} : α → α := (·)

Observe that the codomain is not specified explicitly in these examples. Lean can infer it based on the domain.

The following function taking two arguments ignores the second one.[…]In the context of combinatory logic, this function is called the combinator K.

def K₁ {α β: Type} : α → (β → α) := λ x ↦ (λ _ ↦ x)

Here are some syntactic variations of the same function.

def K₂ {α β: Type} : α → β → α := λ x _ ↦ x

def K₃ {α β : Type} := λ (x : α) (_ : β) ↦ x

def K₄ {α β: Type} (x : α) (_ : β) := x

def K₅ {α β: Type} (x : α) (y : β) := x

Implicit arguments🔗

Let us consider a variant of the identity function taking only implicit arguments.

def I₁' {α : Sort u} : {_ : α} → α := λ {x} ↦ x

Here are two syntactic variations of the same function.

def I₂' {α : Sort u} := λ {x : α} ↦ x

def I₃' {α : Sort u} {x : α} := x

Elimination🔗

Functions are eliminated by application.

example (α β : Type) (f : α → β) (a : α) : β := f a

More generally,

example (I : Sort u) (X : I → Sort v)
  (f : (i : I) → X i) (i : I) :
  X i := f i

Partial application produces a function taking the remaining arguments.

example (α β γ: Type) (f : α → β → γ) (a : α) : β → γ := f a

In contrast, saturated application produces an expression that is not a function.

example (α β γ: Type) (f : α → β → γ) (a : α) (b : β) :
  γ := f a b

Reduction🔗

Recall that having the same normal form is a sufficient condition for two expressions to be definitionally equal. Computing normal forms involves several kinds of reduction. Importantly, an introduction followed immediately by the associated elimination can be reduced. In the case of functions, this is called \beta-reduction.

example (α : Sort u) (β : Sort v) (f : α → β) (a : α) :
  (λ x ↦ f x) a = f a
:= rfl

variable (α : Sort u) (β : Sort v) (f : α → β) (a : α) in
f a#reduce (λ x ↦ f x) a

Although it is not strictly related to functions, we also consider another form of reduction.

delta-reduction🔗

\delta-reduction replaces a defined name by its defining expression.[…]Names are referred to as constants in the Lean Language Reference, see Definitions.

def ℕ2 := ℕ × ℕ

example : ℕ2 = (ℕ × ℕ) := rfl

By default, #reduce does not reduce inside types for performance reasons.

ℕ2#reduce ℕ2

We can force reduction inside types as follows.

ℕ × ℕ#reduce (types := true) ℕ2

The following example combines \beta- and \delta-reduction.

example : I₁ 0 = 0 := rfl

0#reduce I₁ 0

Equality🔗

In addition to reduction, definitional equality identifies certain expressions that differ only by trivial abstraction. This identification is called \eta-equivalence. For functions, \eta-equivalence says that a function is definitionally equal to the \lambda-abstraction obtained by applying the function to an argument.

example (α : Sort u) (β : Sort v) (f : α → β)
  : (λ x ↦ f x) = f
:= rfl

The definitional equality of the left and right-hand sides is not based on them having the same normal form. In fact, the left-hand side does not reduce.

variable (α : Sort u) (β : Sort v) (f : α → β) in
fun x ↦ f x#reduce λ x ↦ f x

Reduction and \eta-equivalence differ in a fundamental way: the former has an intensional nature while the latter is a limited kind of extensionality.

The principle of functional extensionality holds in Lean, but not by rfl.[…]We give a proof of funext later.

example (α : Sort u) (β : Sort v) (f g : α → β)
  (h : ∀ (x : α), f x = g x)
  : f = g
:= funext h

Local definitions🔗

Consider the following function.

def pq₁ (x : ℕ) : ℕ :=
  (x + 1)^2 + 3*(x + 1) + 1

We can avoid repeating the expression x + 1 by composing two functions.

def q (x : ℕ) : ℕ := x + 1
def p (y : ℕ) : ℕ := y^2 + 3*y + 1
def pq₂ (x : ℕ) := p (q x)

This introduces two names p and q. Such auxiliary definitions can be avoided as follows.

def pq₃ (x : ℕ) :=
  have y := x + 1
  y^2 + 3*y + 1

Here have is syntactic sugar for \lambda-abstraction and application.

example (α : Sort u) (β : Sort v) (a : α) (b : β) :
  (
    have x : α := a
    b
  ) = (λ x : α ↦ b) a := rfl

The parentheses around the have syntax can be omitted.

example (α : Sort u) (β : Sort v) (a : α) (b : β) :
  have x : α := a
  b = (λ x : α ↦ b) a := rfl

In particular, the following coincides with pq₂.

def pq₄ (x : ℕ) :=
  (λ y ↦ y^2 + 3*y + 1) (x + 1)

Steps in proofs🔗

A typical use of have is to isolate steps in proofs. Consider the universal instantiation followed by modus ponens.

example (α : Sort u) (P Q : α → Prop)
  (h1 : ∀ a : α, P a → Q a) (h2 : ∀ a : α, P a)
  : ∀ a : α, Q a
:=
  λ a : α ↦
  have Pa := h2 a
  h1 a Pa

We can read the first line of the example as introducing the symbols involved in the statement, which itself consists of the second and third lines. The statement is:

  • Suppose h1 : … and h2 : ….

  • Then ∀ a : α, Q a.

The leading : on the third line reads as "Then" and := on the fourth line as "Proof:".[…]It is due to this reading that we prefer the indentation in the example over the one in Mathlib's style guidelines. When not proving a proposition, we adopt the usual indentation style. The proof has the reading:

  1. Let a : α.

  2. We have P a by hypothesis h2, applied to a.

  3. We conclude by hypothesis h1, applied to a and the fact P a.

Naming every intermediate step can become cumbersome. Omitting the name after have makes the step accessible as this.

example (α : Sort u) (P Q : α → Prop)
  (h1 : ∀ a : α, P a → Q a) (h2 : ∀ a : α, P a)
  : ∀ a : α, Q a
:=
  λ a : α ↦
  have : P a := h2 a
  h1 a this

A proof can also be referenced by its type using ‹…› notation.[…]This introduces no ambiguity, since any two proofs of the same proposition are equal.

example (α : Sort u) (P Q : α → Prop)
  (h1 : ∀ a : α, P a → Q a) (h2 : ∀ a : α, P a)
  : ∀ a : α, Q a
:=
  λ a : α ↦
  have : P a := h2 a
  h1 a ‹P a›

Syntactic abbreviation🔗

A more general abbreviation is given by let.

def pq₅ (x : ℕ) : ℕ :=
  let y := x + 1
  y^2 + 3*y + 1

There are cases where let is applicable but have is not.

def I₇ {α : Sort u} :=
  let t := α
  λ x : t ↦ x

Syntactic abbreviation is unfolded by \zeta-reduction.

example : (let t := ℕ; t × t) = (ℕ × ℕ) := rfl

ℕ × ℕ#reduce (types := true) (let t := ℕ; t × t)

The semicolon is a syntactic device that allows multiple expressions to be written on a single line. Replacing it by a line break leaves the expression intact.

The following example combines \beta-, \delta-, and \zeta-reduction.

example : I₇ 0 = 0 := rfl

0#reduce I₇ 0

On inference of implicit arguments🔗

Implicit arguments let us omit information that Lean can usually infer from the surrounding context. As an illustration, we return to the two syntactic variants I₁ and I₂ of the identity function. They both take an implicit argument, denoted by α in the following examples.

example : {α : Sort u} → α → α := I₁

example : {α : Sort u} → α → α := I₂

In the next example, the implicit argument is inferred to be ℕ.

example : I₁ 0 = I₂ 0 := rfl

The inference can stall if the surrounding context is not informative enough. While α appears on the left-hand side of : in the following example, nothing fixes it on the right-hand side, that is, in I₁ = I₂.

example (α : Sort u) : don't know how to synthesize implicit argument `α`
  @Eq (?m.4 α → ?m.4 α) I₁ I₂
context:
α:Sort u⊢ Sort ?u.5

Note: Because this declaration's type has been explicitly provided, all parameter types and holes (e.g., `_`) in its header are resolved before its body is processed; information from the declaration body cannot be used to infer what these values should bedon't know how to synthesize implicit argument `α`
  @I₁ (?m.4 α)
context:
α:Sort u⊢ Sort ?u.5

Note: Because this declaration's type has been explicitly provided, all parameter types and holes (e.g., `_`) in its header are resolved before its body is processed; information from the declaration body cannot be used to infer what these values should beI₁ = don't know how to synthesize implicit argument `α`
  @I₂ (?m.4 α)
context:
α:Sort u⊢ Sort ?u.5

Note: Because this declaration's type has been explicitly provided, all parameter types and holes (e.g., `_`) in its header are resolved before its body is processed; information from the declaration body cannot be used to infer what these values should beI₂ := rfl

To prove the equality of I₁ and I₂, we must provide more information or use the explicit versions.

example (α : Sort u) : (I₁ : α → α) = I₂ := rfl

example : @I₁ = @I₂ := rfl

Perhaps the cleanest way to specify implicit arguments is to use named arguments.

example (α : Sort u) : I₁ (α := α) = I₂ := rfl

example (β : Sort u) : I₁ (α := β) = I₂ := rfl

The left-hand side of := in (α := α) refers to the argument α in the definition of I₁ and the right-hand side to the argument of the anonymous function defined by the example. The second example renames the latter argument to make the distinction visible.

The variant of the identity function taking only implicit arguments is harder to disambiguate: fixing α alone is not enough.

example (α : Sort u) : don't know how to synthesize implicit argument
  @I₁' α (?m.5 α)
context:
α:Sort u⊢ α

Note: Because this declaration's type has been explicitly provided, all parameter types and holes (e.g., `_`) in its header are resolved before its body is processed; information from the declaration body cannot be used to infer what these values should beI₁' (α := α) = don't know how to synthesize implicit argument `x`
  @I₂' α (?m.6 α)
context:
α:Sort u⊢ α

Note: Because this declaration's type has been explicitly provided, all parameter types and holes (e.g., `_`) in its header are resolved before its body is processed; information from the declaration body cannot be used to infer what these values should beI₂' := rfl

Nonetheless, the explicit versions of I₁' and I₂' coincide.

example : @I₁' = @I₂' := rfl

Unlike I₁', whose second argument is anonymous, the second arguments of I₂' and I₃' are both named x and can be referred to as follows.

example (α : Sort u) (x : α)
  : I₂' (x := x)  = I₃' (x := x) := rfl

Here α can be inferred since it is the type of x.

Rules of the type theory🔗

The following rules govern the concepts introduced so far. They constitute a pure type system. For each rule, we first present an example and then its abstract formulation. In the abstract formulations, we write \operatorname{Sort}_{u} for Sort u, \Pi x : \alpha.\; \beta for (x : α) → β x, and \equiv for definitional equality. Moreover, \Gamma denotes an arbitrary typing context and \beta[x := a] denotes substitution.[…]The substitution replaces all free occurrences of x in the expression \beta with the expression a. We use the standard notation for typing rules.

  1. Axioms

example : Sort (u + 1) := Sort u

\frac{ }{ \vdash \operatorname{Sort}_{u} : \operatorname{Sort}_{u + 1} }

  1. Start

example (α : Sort u) (a : α) : α := a

\frac{ \Gamma \vdash \alpha : \operatorname{Sort}_{u} }{ \Gamma, a : \alpha \vdash a : \alpha }

  1. Weakening

example (α : Sort u) (a : α) (β : Sort v) (b : β) : α := a

\frac{ \Gamma \vdash a : \alpha \quad \Gamma \vdash \beta : \operatorname{Sort}_v }{ \Gamma, b : \beta \vdash a : \alpha }

  1. Product[…]This is the impredicative maximum rule.

example (α : Sort u) (β : α → Sort v) :
  Sort (imax u v) := (x : α) → β x

\frac{ \Gamma \vdash \alpha : \operatorname{Sort}_{u} \quad \Gamma, x : \alpha \vdash \beta : \operatorname{Sort}_{v} }{ \Gamma \vdash \Pi x : \alpha.\; \beta : \operatorname{Sort}_{\operatorname{imax} u \; v} }

  1. Application

example (α : Sort u) (β : α → Sort v)
  (f : (x : α) → β x) (a : α) :
  β a := f a

\frac{ \Gamma \vdash f : \Pi x : \alpha.\; \beta \quad \Gamma \vdash a : \alpha }{ \Gamma \vdash f\; a : \beta[x := a] }

  1. Abstraction

example (α : Sort u) (β : α → Sort v)
  (f : (a : α) → β a) :
  (a : α) → β a := λ x ↦ f x

\frac{ \Gamma, x : \alpha \vdash e : \beta \quad \Gamma \vdash \Pi x : \alpha.\; \beta : \operatorname{Sort}_{v} }{ \Gamma \vdash \lambda x : \alpha \mapsto e : \Pi x : \alpha.\; \beta }

  1. Conversion

example (α : Sort u) (a : α) :
  let β := α
  β := a

\frac{ \Gamma \vdash a : \alpha \quad \Gamma \vdash \alpha\equiv\beta }{ \Gamma \vdash a : \beta }

Further proofs🔗

example : @I₁ = @I₂ := rfl
example : @I₁ = @I₃ := rfl
example : @I₁ = @I₄ := rfl
example : @I₁ = @I₅ := rfl
example : @I₁ = @I₆ := rfl
example : @I₁ = @I₇ := rfl
example : @I₁ = @I₁' := rfl

example : @K₁ = @K₂ := rfl
example : @K₁ = @K₃ := rfl
example : @K₁ = @K₄ := rfl
example : @K₁ = @K₅ := rfl

example : @I₁' = @I₂' := rfl
example : @I₁' = @I₃' := rfl

example : pq₁ = pq₂ := rfl
example : pq₁ = pq₃ := rfl
example : pq₁ = pq₄ := rfl
example : pq₁ = pq₅ := rfl