If you would rather use this time to learn about tactics than about dependently-typed functions, here is an additional practice file for you, on intermediate tactics 🙂.
So far, we have worked mostly with basic functions, such as square : Nat → Nat or fact : Nat → Nat. These are simply-typed functions, from a type to a type.
But in fact, we have also seen type formers, such as Prod : Type → Type → Type or Sum, which takes types as inputs and returns a type. Another common example would be List : Type → Type.
For a fixed type X, the type List X is defined inductively: you start with the empty list and then prepend terms of type X.
inductive List (X : Type) : Type
| nil : List X
| cons : X → List X → List X
So far, we have seen:
There should also be:
A polymorphic function is a function that takes a type as an argument. The simplest polymorphic function is the identity function:
def id {X : Type} : X → X := fun x ↦ x
#check id -- id {X : Type} : X → X
id is indeed a term that depends (implicitely) on a type. For instance, id (X := Nat) (also denoted by @id Nat) is a term of type Nat → Nat, i.e. a function from Nat to Nat.
id is an example of an overloaded term: whatever the type X, we may denote its identity function simply by id, so an expression such as id (3 : Nat) is well-typed (and of type Nat) and so is the expression id (3 : Int).
X is a function F: X → Type, so indeed types that depend on terms: for all x : X, F x is a type.Tuples : Nat → Type such that Tuples n is the type of lists of integers of length n:Tuples n := {L : List Int // List.length L = n}.List Int, which you can picture as the sum (or disjoint union) of all the Tuples n.F: X → Type, we can form the associated (x : X) × F x, whose terms are called dependent pairs and are of the form ⟨x, t⟩ where x : X and t : F x.⟨2, [1, -1]⟩ is a term of type (n : Nat) × Tuples n. Another possible notation would be Sigma Tuples, where Tuples : Nat → Type. And the classical type-theoretic notation would be:x : X, we have F x = Y, then(x : X) × F x = X × Y.Given a type family F: X → Type, the associated Sigma F defined in this way can be denoted by (x : X) × F x.
inductive Sigma {X : Type} (F : X → Type) : Type
| mk (x : X) (t : F x) : Sigma F
Projection to the first factor can be defined by pattern matching (which under the hood calls upon the induction principle associated to the inductive type Sigma F):
def pr₁ {X : Type} {F : X → Type} : Sigma F → X
| Sigma.mk x t => x
Type families can also be declared inductively. Vectors for instance, can be introduced as follows, mimicking the construction for lists.
Let X be a type. We consider the type family Vec X : Nat → Type defined inductively as follows:
inductive Vec (X : Type) : Nat → Type
| null : Vec X 0
| cons {n :Nat} (x : X) (v : Vec X n) : Vec X (n + 1)
Here Vec X n represents the type of lists of length n with entries in X:
Vec.null is a list of length 0.n : Nat, cons x (v : Vec X n) is a list of length n + 1.F : X → Type to define so-called f : (x : X) → F x whose return type depends on the input value.(x : X) → F x isx : X, we have F x = Y, then(x : X) → F x = X → Y.zero-tuple : (n : Nat) → Tuples n. We proceed by induction.n = 0, we want zero-tuple 0 to be the dependent pair ⟨l, p⟩ consisting of the empty list ([] : List Int) and a proof that the empty list has length 0.n = k + 1, we want zero-tuple n to be the dependent pair ⟨l, p⟩ consisting of the list (0 :: zero-tuples k) and a proof that this list has length k + 1.A
def pr₂ {X : Type} {F : X → Type} : (p : Sigma F) → F (pr₁ p)
| Sigma.mk x t => t
We can use this projection to establish an equivalence between dependent functions of the form
F : X → Type by a predicate P : X → Prop.(x : X) ×' P x or by {x : X // P x}.⟨x, px⟩ where x is an element of X and px is a proof of the proposition P x. This is an important to recall when defining functions out of a subtype.{L : List X // L.length = n} of List X and the type Vec X n.Given a predicate P : X → Prop on a type X, the proposition ∃ x : X, P x is the proposition defined inductively as follows.
inductive Exists {X : Type} (P : X → Prop) : Prop
| intro (x : X) (p : P x) : Exists P
This means that, in order to prove that ∃ x : X, P x, you need to construct a term x : X (a witness) and a proof of the proposition P x (the evidence).
Note the analogy with subtypes:
inductive Subtype {X : Type} (P : X → Prop) : Type
| intro (x : X) (p : P x) : Subtype P
Also note that ∃ x : X, P x is stronger than saying ¬(∀ x : X, ¬(P x)).
Given a predicate P : X → Prop on a type X, the proposition ∀ x : X, P x is the proposition defined by the type of dependent functions (x : X) → P x.
This means that, to prove such a statement, you need to construct a function. So you start your proof with fun x ↦ _ (if you are in term mode) or intro x (if you are in tactic mode).
example : ∀ w : ℂ, ∃ z : ℂ, z ^ 2 = w :=
by -- ⊢ ∀ w : ℂ, ∃ z : ℂ, z ^ 2 = w
intro w -- w : ℂ ⊢ ∃ z : ℂ, z ^ 2 = w
sorry
Note that there might be more than one witness z : ℂ for the property z ^ 2 = w, but that piece of data cannot be recovered from the existential statement itself.
It is possible to use usual mathematical notation in Lean. The quantifier symbols unfold to the definitions that we have given before and it is a good exercise to convert the types represented below into Σ-types and Π-types (by definition, Sequence ℝ := ℕ → ℝ).
def Sequence.isConvergent (s : Sequence ℝ) : Prop :=
∃ l : ℝ, ∀ ε > 0, ∃ n : ℕ, ∀ m : Nat, m ≥ n → |s m - l| < ε
def Sequence.isStationary (s : Sequence ℝ) : Prop :=
∃ a : ℝ, ∃ n : ℕ, ∀ m : Nat, m ≥ n → s m = a
A good exercise is to prove the following implication.
theorem stationary_implies_convergent :
∀ (s : ℕ → ℝ), s.isStationary → s.isConvergent := sorry
Besides simply-typed functions, we can define type formers, polymorphic functions and type families.
To a type family F : X → Type, we can associate:
(x : X) × F x, which is a (x : X) → F x, which is a Given a predicate P : X → Prop, the proposition ∃ x : X, P x is the proposition whose proofs are constructed using dependent pairs (x : X) ×' P x. And a proof of the proposition ∀ x : X, P x is a dependent function, of type (x : X) → P x.
Now we can prove state some theorems 💻 ! Using dependent types, we can actually formalise a remarkable amount of mathematics.
Here is the practice files on intermediate tactics again, as well as a short exercise on convergent sequences. I am happy to answer any questions you may have 😊.
Thank you for your attention!