Chapter 2: Dependent Type Theory
Lean is based on a version of dependent type theory known as the Calculus of Constructions, with a countable hierarchy of non-cumulative universes and inductive types.
Prefixing a command with a hash (#
) turns it into an auxiliary command,
allowing you to query the system.
The #eval
command asks Lean to evaluate an expression.
Use Unicode for an arrow →
(\r
) instead of the ASCII ->
Use the Unicode for product ×
(\x
) instead of the ASCII x
Use the Unicode for alpha α
(\a
), beta β (\b
), and gamma γ (\g
)
Nat ×Nat: TypeNat →Nat: TypeNatNat: Type(Nat →Nat: TypeNat) →Nat: TypeNat -- a "functional"Nat: Type
Whitespace is function application.
Arrows are right-associative.
defα :α: TypeType :=Type: Type 1Nat defNat: Typeβ :β: TypeType :=Type: Type 1Bool defBool: TypeF :F: Type → TypeType →Type: Type 1Type :=Type: Type 1List defList: Type → TypeG :G: Type → Type → TypeType →Type: Type 1Type →Type: Type 1Type :=Type: Type 1ProdProd: Type → Type → Typeα -- Typeα: TypeFF: Type → Typeα -- Typeα: TypeFF: Type → TypeNat -- TypeNat: TypeGG: Type → Type → Typeα -- Type → Typeα: TypeGG: Type → Type → Typeαα: Typeβ -- Typeβ: TypeGG: Type → Type → Typeαα: TypeNat -- Type -- Prod is a type constructor -- the infix version is ×Nat: TypeProd -- Type → Type → TypeProd: Type u_1 → Type u_2 → Type (max u_1 u_2)ProdProd: Type → Type → Typeαα: Typeβ -- Typeβ: Typeα ×α: Typeβ -- Typeβ: TypeProdProd: Type → Type → TypeNatNat: TypeNat -- TypeNat: TypeNat ×Nat: TypeNat -- TypeNat: Type
Lean has a universe of types indexed by the natural numbers. Type
is
an abbreviation for Type 0
.
We can think of Type 0
as a universe of "small" types. Type 1
is a universe
of larger types, which contains Type 0
. Likewise, Type 2
is a larger-still
universe of types, containing Type 1
as an element.
Some functions, like List
and Prod
, are polymorphic over the universe of
types.
ListList: Type u_1 → Type u_1ProdProd: Type u_1 → Type u_2 → Type (max u_1 u_2)
We can define polymorphic constants using the universe
command.
universe u₁ defF₁ (F₁: Type u₁ → Type u₁α :α: Type u₁Type u₁) :Type u₁: Type (u₁ + 1)Type u₁ :=Type u₁: Type (u₁ + 1)ProdProd: Type u₁ → Type u₁ → Type u₁αα: Type u₁αα: Type u₁F₁ -- Type u → Type uF₁: Type u_1 → Type u_1
We can avoid universe
by providing the universe parameters when defining
the function.
defF₂F₂: Type u₂ → Type u₂.{u₂} (F₂.{u₂}: Type u₂ → Type u₂α :α: Type u₂Type u₂) :Type u₂: Type (u₂ + 1)Type u₂ :=Type u₂: Type (u₂ + 1)ProdProd: Type u₂ → Type u₂ → Type u₂αα: Type u₂αα: Type u₂F₂ -- Type u → Type uF₂: Type u_1 → Type u_1
Function Abstraction and Evaluation
In Lean, fun
and λ
are the same.
Definition: Alpha Equivalence
Expressions which are the same up to renaming of bound variables are alpha equivalent.
Definition: Definitional Equivalence
Two terms which reduce to the same value are definitionally equivalent.
The command #eval
executes expressions and is the preferred way of testing
your functions.
Variables and Sections
Lean has a variable
command which declares variables of any type. For example,
def compose₁: (α β γ : Type) → (β → γ) → (α → β) → α → γ
compose₁ (α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type) (g: β → γ
g : β: Type
β → γ: Type
γ) (f: α → β
f : α: Type
α → β: Type
β) (x: α
x : α: Type
α) : γ: Type
γ :=
g: β → γ
g (f: α → β
f x: α
x)
def doTwice₁: (α : Type) → (α → α) → α → α
doTwice₁ (α: Type
α : Type: Type 1
Type) (h: α → α
h : α: Type
α → α: Type
α) (x: α
x : α: Type
α) : α: Type
α :=
h: α → α
h (h: α → α
h x: α
x)
def doThrice₁: (α : Type) → (α → α) → α → α
doThrice₁ (α: Type
α : Type: Type 1
Type) (h: α → α
h : α: Type
α → α: Type
α) (x: α
x : α: Type
α) : α: Type
α :=
h: α → α
h (h: α → α
h (h: α → α
h x: α
x))
becomes
variable (α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type)
def compose₂: (β → γ) → (α → β) → α → γ
compose₂ (g: β → γ
g : β: Type
β → γ: Type
γ) (f: α → β
f : α: Type
α → β: Type
β) (x: α
x : α: Type
α) : γ: Type
γ :=
g: β → γ
g (f: α → β
f x: α
x)
def doTwice₂: (α : Type) → (α → α) → α → α
doTwice₂ (h: α → α
h : α: Type
α → α: Type
α) (x: α
x : α: Type
α) : α: Type
α :=
h: α → α
h (h: α → α
h x: α
x)
def doThrice₂: (α : Type) → (α → α) → α → α
doThrice₂ (h: α → α
h : α: Type
α → α: Type
α) (x: α
x : α: Type
α) : α: Type
α :=
h: α → α
h (h: α → α
h (h: α → α
h x: α
x))
Lean has a section
command which provides a scoping mechanism. When we
close a section, the variables defined within are out of scope. You do not need
to name a section, or indent its contents.
section useful
variable (α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type)
variable (g: β → γ
g : β: Type
β → γ: Type
γ) (f: α → β
f : α: Type
α → β: Type
β) (h: α → α
h : α: Type
α → α: Type
α)
variable (x: α
x : α: Type
α)
def compose: γ
compose := g: β → γ
g (f: α → β
f x: α
x)
def doTwice: (α : Type) → (α → α) → α → α
doTwice := h: α → α
h (h: α → α
h x: α
x)
def doThrice: α
doThrice := h: α → α
h (h: α → α
h (h: α → α
h x: α
x))
end useful
Namespaces
Namespaces let you group related definitions. Like sections, they can be nested. However, unlike sections, they require a name, and can be reopened later!
The intended usage is this: namespaces organize data and sections declare
variables for use in definitions or delimit the scope of commands like
set_option
and open
.
namespace Foo defa :a: NatNat :=Nat: Type5 def5: Natf (f: Nat → Natx :x: NatNat) :Nat: TypeNat :=Nat: Typex +x: Nat7 def7: Natfa :fa: NatNat :=Nat: Typeff: Nat → Nata defa: Natffa :ffa: NatNat :=Nat: Typef (f: Nat → Natff: Nat → Nata)a: Nataa: Natff: Nat → Natfafa: Natffaffa: NatFoo.fa end Foo -- #check a -- error -- #check f -- errorFoo.fa: NatFoo.aFoo.a: NatFoo.fFoo.f: Nat → NatFoo.faFoo.fa: NatFoo.ffaFoo.ffa: Nat
The open
command brings the contents of the namespace into scope.
open Fooaa: Natff: Nat → Natfafa: NatFoo.faFoo.fa: Nat
What makes dependent type theory dependent?
Types can depend on parameters! For example, the type Vector α n
is the type of vectors with elements α : Type
and length n : Nat
.
If you were to write a function cons
which prepends an element to a list, the
function would need to be polymorphic over the type of the list. One could
imagine that cons α
would be the function which prepends an element to a
List α
. So cons α
would have type α → List α → List α
. But what about
cons
? It cannot have the type of Type → α → List α → List α
because α
is
unrelated to Type
. We need to make the type of the elements of the list
depend on α
: cons : (α : Type) → α → List α → List α
.
defcons (cons: (α : Type) → α → List α → List αα :α: TypeType) (Type: Type 1a :a: αα) (α: Typeas :as: List αListList: Type → Typeα) :α: TypeListList: Type → Typeα :=α: TypeList.consList.cons: {α : Type} → α → List α → List αaa: αasas: List αconscons: (α : Type) → α → List α → List αNat -- Nat → List Nat → List NatNat: Typeconscons: (α : Type) → α → List α → List αBool -- Bool → List Bool → List BoolBool: Typecons -- (α : Type) → α → List α → List αcons: (α : Type) → α → List α → List α
This is an instance of the dependent arrow type.
Given α : Type
and β : α → Type
, we can think of β
as a family of types
indexed by α
. That is, for each a : α
, we have a type β a
. Then
(a : α) → β a
is the type of functions f
with the property that f a
is
an element of the type β a
and the type of the value returned by f
depends
on its input.
Note: When the value of β
depends on a
, (a : α) → β
is called a dependent
function type. When β
doesn't depend on a
, (a : α) → β
is equivalent to
α → β
. In dependent type theory, α → β
is the notation used when β
does
not depend on a
.
We can use the #check
command to inspect the type of the following functions
from List
.
@List.cons -- {α : Type u_1} → α → List α → List αList.cons: {α : Type u_1} → α → List α → List α@List.nil -- {α : Type u_1} → List αList.nil: {α : Type u_1} → List α@List.length -- {α : Type u_1} → List α → NatList.length: {α : Type u_1} → List α → Nat@List.append -- {α : Type u_1} → List α → List α → List αList.append: {α : Type u_1} → List α → List α → List α
In the same way that the dependent arrow type generalizes the function type
α → β
by allowing β
to depend on α
, the dependent Cartesian product type
(a : α) × β a
generalizes the cartesian product α × β
. Dependent products
are also called sigma types and are written as Σ a : α, β a
. You can use
⟨a, b⟩
(those are angle brackets, which you can insert with \langle
(or
\lan
) and \rangle
(or \ran
)), or Sigma.mk a b
to construct a dependent
pair.
TO-DO: Is there another name for dependent function types if dependent Cartesian products are called sigma types? How do dependent pairs relate to dependent Cartesian products?
universe u v deff (f: (α : Type u) → (β : α → Type v) → (a : α) → β a → (a : α) × β aα :α: Type uType u) (Type u: Type (u + 1)β :β: α → Type vα →α: Type uType v) (Type v: Type (v + 1)a :a: αα) (α: Type ub :b: β aββ: α → Type va) : (a: αa :a: αα) ×α: Type uββ: α → Type va := ⟨a: αa,a: αb⟩ defb: β ag (g: (α : Type u) → (β : α → Type v) → (a : α) → β a → (a : α) × β aα :α: Type uType u) (Type u: Type (u + 1)β :β: α → Type vα →α: Type uType v) (Type v: Type (v + 1)a :a: αα) (α: Type ub :b: β aββ: α → Type va) : Σa: αa :a: αα,α: Type uββ: α → Type va :=a: αSigma.mkSigma.mk: {α : Type u} → {β : α → Type v} → (fst : α) → β fst → Sigma βaa: αb defb: β ah1 (h1: Nat → Natx :x: NatNat) :Nat: TypeNat := (Nat: Typeff: (α : Type 1) → (β : α → Type) → (a : α) → β a → (a : α) × β aType (funType: Type 1α =>α: Typeα)α: TypeNatNat: Typex).x: Nat22: {α : Type 1} → {β : α → Type} → (self : Sigma β) → β self.fsth1h1: Nat → Nat5 -- 5 def5: Nath2 (h2: Nat → Natx :x: NatNat) :Nat: TypeNat := (Nat: Typegg: (α : Type 1) → (β : α → Type) → (a : α) → β a → (a : α) × β aType (funType: Type 1α =>α: Typeα)α: TypeNatNat: Typex).x: Nat22: {α : Type 1} → {β : α → Type} → (self : Sigma β) → β self.fsth2h2: Nat → Nat5 -- 55: Nat
The functions f
and g
above are the same function.
Implicit Arguments
Omitted. See the original here.