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.