Copyright © 2018–2023 Anne Baanen, Alexander Bentkamp, Jasmin Blanchette, Johannes Hölzl, and Jannis Limperg. See LICENSE.txt.

import LoVe.Lectures.LoVe06_InductivePredicates_Demo

LoVe Demo 12: Logical Foundations of Mathematics

We dive deeper into the logical foundations of Lean. Most of the features described here are especially relevant for defining mathematical objects and proving theorems about them.

namespace LoVe

Universes

Not only terms have a type, but also types have a type. For example,

`@And.intro : ∀a b, a → b → a ∧ b`

and

`∀a b, a → b → a ∧ b : Prop`

Now, what is the type of Prop? Prop has the same type as virtually all other types we have constructed so far:

`Prop : Type`

What is the type of Type? The typing Type : Type would lead to a contradiction, called Girard's paradox, resembling Russell's paradox. Instead:

`Type   : Type 1`
`Type 1 : Type 2`
`Type 2 : Type 3`
⋮

Aliases:

`Type`   := `Type 0`
`Prop`   := `Sort 0`
`Type u` := `Sort (u + 1)`

The types of types (Sort u, Type u, and Prop) are called universes. The u in Sort u is a universe level.

The hierarchy is captured by the following typing judgment:

————————————————————————— Sort
C ⊢ Sort u : Sort (u + 1)
#check @And.intro
#check ∀a b : Prop, a → b → a ∧ b
#check Prop
#check ℕ
#check Type
#check Type 1
#check Type 2

universe u v

#check Type u

#check Sort 0
#check Sort 1
#check Sort 2
#check Sort u

#check Type _

#check ULift
#check ULift.{32, _} ℕ

An interlude: let's talk about Russell's paradox (in another file!)

The Peculiarities of Prop

Prop is different from the other universes in many respects.

Impredicativity

The function type σ → τ is put into the larger one of the universes that σ and τ live in:

C ⊢ σ : Type u    C ⊢ τ : Type v
————————————————————————————————— SimpleArrow-Type
C ⊢ σ → τ : Type (max u v)

For dependent types, this generalizes to

C ⊢ σ : Type u    C, x : σ ⊢ τ[x] : Type v
——————————————————————————————————————————— Arrow-Type
C ⊢ (x : σ) → τ[x] : Type (max u v)

This behavior of the universes Type v is called predicativity: roughly, "non-self-referencing."

To force expressions such as ∀a : Prop, a → a to be of type Prop anyway, we need a special typing rule for Prop:

C ⊢ σ : Sort u    x : σ ⊢ τ[x] : Prop
—————————————————————————————————————— Arrow-Prop
C ⊢ (∀x : σ, τ[x]) : Prop

This behavior of Prop is called impredicativity.

The rules Arrow-Type and Arrow-Prop can be generalized into a single rule:

C ⊢ σ : Sort u    C, x : σ ⊢ τ[x] : Sort v
——————————————————————————————————————————— Arrow
C ⊢ (x : σ) → τ[x] : Sort (imax u v)

where

`imax u 0       = 0`
`imax u (v + 1) = max u (v + 1)`
#check fun (α : Type u) (β : Type v) ↦ α → β
#check ∀a : Prop, a → a

Proof Irrelevance

A second difference between Prop and Type u is proof irrelevance:

`∀(a : Prop) (h₁ h₂ : a), h₁ = h₂`

This makes reasoning about dependent types easier.

When viewing a proposition as a type and a proof as an element of that type, proof irrelevance means that a proposition is either an empty type or has exactly one inhabitant.

Proof irrelevance can be proved by rfl.

#check proof_irrel

theorem proof_irrel {a : Prop} (h₁ h₂ : a) :
  h₁ = h₂ :=
  by rfl

No Large Elimination

A further difference between Prop and Type u is that Prop does not allow large elimination, meaning that it impossible to extract data from a proof of a proposition.

This is necessary to allow proof irrelevance.

-- fails def unsquare (i : ℤ) (hsq : ∃j, i = j * j) : ℤ := match hsq with | Exists.intro j _ => j

If the above were accepted, we could derive False as follows.

Let

`hsq₁` := `Exists.intro 3 (by linarith)`
`hsq₂` := `Exists.intro (-3) (by linarith)`

be two proofs of ∃j, (9 : ℤ) = j * j. Then

`unsquare 9 hsq₁ = 3`
`unsquare 9 hsq₂ = -3`

However, by proof irrelevance, hsq₁ = hsq₂. Hence

`unsquare 9 hsq₂ = 3`

Thus

`3 = -3`

a contradiction.

As a compromise, Lean allows small elimination. It is called small elimination because it eliminates only into Prop, whereas large elimination can eliminate into an arbitrary large universe Sort u. This means we can use match to analyze the structure of a proof, extract existential witnesses, and so on, as long as the match expression is itself a proof. We have seen several examples of this in lecture 5.

As a further compromise, Lean allows large elimination for syntactic subsingletons: types in Prop for which it can be established syntactically that they have cardinality 0 or 1. These are propositions such as False and a ∧ b that can be proved in at most one way.

Impredicative, proof-irrelevant Prop

Coquand has recently shown that having an impredicative, proof-irrelevant universe leads to a failure of strong normalization. https://arxiv.org/pdf/1911.08174.pdf

Lean code adapted from Mario Carneiro.

namespace no_sn

def false' : Prop :=
∀ p : Prop, p

def true' : Prop :=
false' → false'

theorem omega : (∀ (A B : Prop), A = B) → false' :=
fun h A => @cast true' _ (h true' A) (fun z: false' => z true' z)

theorem Omega : (∀ (A B : Prop), A = B) → false' :=
fun h => omega h true' (omega h)

-- #reduce Omega -- timeout

example (h : ∀ A B : Prop, A = B) : Omega h = Omega h :=
calc
  Omega h = omega h true' (omega h) := rfl
  _       = @cast true' true' (h true' true') (fun z: false' => z true' z) (omega h) := rfl
  _       = (fun z: false' => z true' z) (omega h) := rfl
  _       = (omega h) true' (omega h) := rfl
  _       = Omega h := rfl

end no_sn

The Axiom of Choice

Lean's logic includes the axiom of choice, which makes it possible to obtain an arbitrary element from any nonempty type.

Consider Lean's Nonempty inductive predicate:

#print Nonempty

The predicate states that α has at least one element.

To prove Nonempty α, we must provide an α value to Nonempty.intro:

theorem Nat.Nonempty :
  Nonempty ℕ :=
  Nonempty.intro 0

Since Nonempty lives in Prop, large elimination is not available, and thus we cannot extract the element that was used from a proof of Nonempty α.

The axiom of choice allows us to obtain some element of type α if we can show Nonempty α:

#print Classical.choice

It will just give us an arbitrary element of α; we have no way of knowing whether this is the element that was used to prove Nonempty α.

The constant Classical.choice is noncomputable, which is why some logicians prefer to work without this axiom.

#eval Classical.choice Nat.Nonempty -- fails

#reduce Classical.choice Nat.Nonempty

The axiom of choice is only an axiom in Lean's core library, giving users the freedom to work with or without it.

Definitions using it must be marked as noncomputable:

noncomputable def arbitraryNat : ℕ :=
  Classical.choice Nat.Nonempty

The following tools rely on choice.

Law of Excluded Middle

#check Classical.em
#check Classical.byContradiction

Hilbert Choice

#print Classical.choose
#print Classical.choose_spec

#check fun (p : ℕ → Prop) (h : ∃n : ℕ, p n) ↦
  Classical.choose h
#check fun (p : ℕ → Prop) (h : ∃n : ℕ, p n) ↦
  Classical.choose_spec h

Set-Theoretic Axiom of Choice

#print Classical.axiomOfChoice

Subtypes

Subtyping is a mechanism to create new types from existing ones.

Given a predicate on the elements of the base type, the subtype contains only those elements of the base type that fulfill this property. More precisely, the subtype contains element–proof pairs that combine an element of the base type and a proof that it fulfills the property.

Subtyping is useful for those types that cannot be defined as an inductive type. For example, any attempt at defining the type of finite sets along the following lines is doomed to fail:

-- wrong
inductive Finset (α : Type) : Type
  | empty  : Finset α
  | insert : α → Finset α → Finset α

Why does this not model finite sets?

Given a base type and a property, the subtype has the syntax

`{` _variable_ `:` _base-type_ `//` _property-applied-to-variable_ `}`

Alias:

`{x : τ // P[x]}` := `@Subtype τ (fun x ↦ P[x])`

Examples:

`{i : ℕ // i ≤ n}`            := `@Subtype ℕ (fun i ↦ i ≤ n)`
`{A : Set α // Set.Finite A}` := `@Subtype (Set α) Set.Finite`

First Example: Full Binary Trees

#check BTree
#check IsFull
#check mirror
#check IsFull_mirror
#check mirror_mirror

def FullBTree (α : Type) : Type :=
  {t : BTree α // IsFull t}

#print Subtype
#check Subtype.mk

To define elements of FullBTree, we must provide a BTree and a proof that it is full:

def emptyFullBTree : FullBTree ℕ :=
  Subtype.mk BTree.empty IsFull.empty

def fullBTree6 : FullBTree ℕ :=
  Subtype.mk (BTree.node 6 BTree.empty BTree.empty)
    (by
       apply IsFull.node
       apply IsFull.empty
       apply IsFull.empty
       rfl)

#reduce Subtype.val fullBTree6
#check Subtype.property fullBTree6

We can lift existing operations on BTree to FullBTree:

def FullBTree.mirror {α : Type} (t : FullBTree α) :
  FullBTree α :=
  Subtype.mk (LoVe.mirror (Subtype.val t))
    (by
       apply IsFull_mirror
       apply Subtype.property t)

#reduce Subtype.val (FullBTree.mirror fullBTree6)

And of course we can prove theorems about the lifted operations:

theorem FullBTree.mirror_mirror {α : Type} (t : FullBTree α) :
  (FullBTree.mirror (FullBTree.mirror t)) = t :=
  by
    apply Subtype.eq
    simp [FullBTree.mirror, LoVe.mirror_mirror]

#check Subtype.eq

Second Example: Vectors

def Vector (α : Type) (n : ℕ) : Type :=
  {xs : List α // List.length xs = n}

def vector123 : Vector ℤ 3 :=
  Subtype.mk [1, 2, 3] (by rfl)

def Vector.neg {n : ℕ} (v : Vector ℤ n) : Vector ℤ n :=
  Subtype.mk (List.map Int.neg (Subtype.val v))
    (by
       rw [List.length_map]
       exact Subtype.property v)

theorem Vector.neg_neg (n : ℕ) (v : Vector ℤ n) :
  Vector.neg (Vector.neg v) = v :=
  by
    apply Subtype.eq
    simp [Vector.neg]

Quotient Types

Quotients are a powerful construction in mathematics used to construct , , , and many other types.

Like subtyping, quotienting constructs a new type from an existing type. Unlike a subtype, a quotient type contains all of the elements of the base type, but some elements that were different in the base type are considered equal in the quotient type. In mathematical terms, the quotient type is isomorphic to a partition of the base type.

To define a quotient type, we need to provide a type that it is derived from and an equivalence relation on the type that determines which elements are considered equal.

#check Quotient
#print Setoid

#check Quotient.mk
#check Quotient.sound
#check Quotient.exact

#check Quotient.lift
#check Quotient.lift₂
#check @Quotient.inductionOn

First Example: Integers

Let us build the integers as a quotient over pairs of natural numbers ℕ × ℕ.

A pair (p, n) of natural numbers represents the integer p - n. Nonnegative integers p can be represented by (p, 0). Negative integers -n can be represented by (0, n). However, many representations of the same integer are possible; e.g., (7, 0), (8, 1), (9, 2), and (10, 3) all represent the integer 7.

Which equivalence relation can we use?

We want two pairs (p₁, n₁) and (p₂, n₂) to be equal if p₁ - n₁ = p₂ - n₂. However, this does not work because subtraction on is ill-behaved (e.g., 0 - 1 = 0). Instead, we use p₁ + n₂ = p₂ + n₁.

instance Int.Setoid : Setoid (ℕ × ℕ) :=
  { r :=
      fun pn₁ pn₂ : ℕ × ℕ ↦
        Prod.fst pn₁ + Prod.snd pn₂ =
        Prod.fst pn₂ + Prod.snd pn₁
    iseqv :=
      { refl :=
          by
            intro pn
            rfl
        symm :=
          by
            intro pn₁ pn₂ h
            rw [h]
        trans :=
          by
            intro pn₁ pn₂ pn₃ h₁₂ h₂₃
            linarith } }

theorem Int.Setoid_Iff (pn₁ pn₂ : ℕ × ℕ) :
  pn₁ ≈ pn₂ ↔
  Prod.fst pn₁ + Prod.snd pn₂ = Prod.fst pn₂ + Prod.snd pn₁ :=
  by rfl

def Int : Type :=
  Quotient Int.Setoid

def Int.zero : Int :=
  ⟦(0, 0)⟧

theorem Int.zero_Eq (m : ℕ) :
  Int.zero = ⟦(m, m)⟧ :=
  by
    rw [Int.zero]
    apply Quotient.sound
    rw [Int.Setoid_Iff]
    simp

def Int.add : Int → Int → Int :=
  Quotient.lift₂
    (fun pn₁ pn₂ : ℕ × ℕ ↦
       ⟦(Prod.fst pn₁ + Prod.fst pn₂,
         Prod.snd pn₁ + Prod.snd pn₂)⟧)
    (by
       intro pn₁ pn₂ pn₁' pn₂' h₁ h₂
       apply Quotient.sound
       rw [Int.Setoid_Iff] at *
       linarith)

theorem Int.add_Eq (p₁ n₁ p₂ n₂ : ℕ) :
  Int.add ⟦(p₁, n₁)⟧ ⟦(p₂, n₂)⟧ = ⟦(p₁ + p₂, n₁ + n₂)⟧ :=
  by rfl

theorem Int.add_zero (i : Int) :
  Int.add Int.zero i = i :=
  by
    induction i using Quotient.inductionOn with
    | h pn =>
      cases pn with
      | mk p n => simp [Int.zero, Int.add_Eq]

This definitional syntax would be nice:

-- fails def Int.add : Int → Int → Int | ⟦(p₁, n₁)⟧, ⟦(p₂, n₂)⟧ => ⟦(p₁ + p₂, n₁ + n₂)⟧

But it would be dangerous:

-- fails def Int.fst : Int → ℕ | ⟦(p, n)⟧ => p

Using Int.fst, we could derive False. First, we have

`Int.fst ⟦(0, 0)⟧ = 0`
`Int.fst ⟦(1, 1)⟧ = 1`

But since ⟦(0, 0)⟧ = ⟦(1, 1)⟧, we get

`0 = 1`

Second Example: Unordered Pairs

Unordered pairs are pairs for which no distinction is made between the first and second components. They are usually written {a, b}.

We will introduce the type UPair of unordered pairs as the quotient of pairs (a, b) with respect to the relation "contains the same elements as".

instance UPair.Setoid (α : Type) : Setoid (α × α) :=
{ r :=
    fun ab₁ ab₂ : α × α ↦
      ({Prod.fst ab₁, Prod.snd ab₁} : Set α) =
      ({Prod.fst ab₂, Prod.snd ab₂} : Set α)
  iseqv :=
    { refl  := by simp
      symm  := by aesop
      trans := by aesop } }

theorem UPair.Setoid_Iff {α : Type} (ab₁ ab₂ : α × α) :
  ab₁ ≈ ab₂ ↔
  ({Prod.fst ab₁, Prod.snd ab₁} : Set α) =
  ({Prod.fst ab₂, Prod.snd ab₂} : Set α) :=
  by rfl

def UPair (α : Type) : Type :=
  Quotient (UPair.Setoid α)

#check UPair.Setoid

It is easy to prove that our pairs are really unordered:

theorem UPair.mk_symm {α : Type} (a b : α) :
  (⟦(a, b)⟧ : UPair α) = ⟦(b, a)⟧ :=
  by
    apply Quotient.sound
    rw [UPair.Setoid_Iff]
    simp [Set.unordered_pair_comm]

Another representation of unordered pairs is as sets of cardinality 1 or 2. The following operation converts UPair to that representation:

def Set_of_UPair {α : Type} : UPair α → Set α :=
  Quotient.lift (fun ab : α × α ↦ {Prod.fst ab, Prod.snd ab})
    (by
       intro ab₁ ab₂ h
       rw [UPair.Setoid_Iff] at *
       exact h)

Alternative Definitions via Normalization and Subtyping

Each element of a quotient type corresponds to an -equivalence class. If there exists a systematic way to obtain a canonical representative for each class, we can use a subtype instead of a quotient, keeping only the canonical representatives.

Consider the quotient type Int above. We could say that a pair (p, n) is canonical if p or n is 0.

namespace Alternative

inductive Int.IsCanonical : ℕ × ℕ → Prop
  | nonpos {n : ℕ} : Int.IsCanonical (0, n)
  | nonneg {p : ℕ} : Int.IsCanonical (p, 0)

def Int : Type :=
  {pn : ℕ × ℕ // Int.IsCanonical pn}

Normalizing pairs of natural numbers is easy:

def Int.normalize : ℕ × ℕ → ℕ × ℕ
  | (p, n) => if p ≥ n then (p - n, 0) else (0, n - p)

theorem Int.IsCanonical_normalize (pn : ℕ × ℕ) :
  Int.IsCanonical (Int.normalize pn)
:=
  by
    cases pn with
    | mk p n =>
      simp [Int.normalize]
      cases Classical.em (p ≥ n) with
      | inl hpn =>
        simp [*]
        exact Int.IsCanonical.nonneg
      | inr hpn =>
        simp [*]
        exact Int.IsCanonical.nonpos

For unordered pairs, there is no obvious normal form, except to always put the smaller element first (or last). This requires a linear order on α.

def UPair.IsCanonical {α : Type} [LinearOrder α] :
  α × α → Prop
  | (a, b) => a ≤ b

def UPair (α : Type) [LinearOrder α] : Type :=
  {ab : α × α // UPair.IsCanonical ab}

end Alternative

end LoVe