Theorem Proving in Lean 4

12. 公理与计算🔗

我们已经看到,Lean 中实现的构造演算(Calculus of Constructions)版本包含了依值函数类型、归纳类型(inductive types)以及一个以位于底层的非直谓的(impredicative)、证明无关的(proof-irrelevant)Prop为起点的宇宙(universe)层级结构。在本章中,我们考虑用额外的公理(axioms)和规则(rules)来扩展 CIC 的方式。以这种方式扩展一个基础系统通常很方便;它可以使我们证明更多的定理(theorems),也可以使原本能够证明的定理更容易证明。但添加额外的公理也可能带来负面后果,这些后果可能超出了对其正确性的担忧。特别是,公理的使用会影响定义(definitions)和定理的计算内容(computational content),我们将在此探讨这一点。

Lean 被设计为同时支持计算(computational)和经典(classical)推理。有需要的用户可以使用“计算纯”(computationally pure)片段,它保证系统中封闭表达式(closed expressions)会求值到规范范式(canonical normal forms)。特别地,任何封闭的计算纯表达式,如果类型为Nat,例如,都会归约为一个数字。

Lean 的标准库定义了一个额外的公理——命题外延性(propositional extensionality),以及一个商构造(quotient construction),后者又蕴涵了函数外延性(function extensionality)原理。这些扩展被用于例如开发集合和有限集的理论。我们将在下面看到,使用这些定理会阻塞 Lean 内核(kernel)中的求值,因此类型为Nat的封闭项不再会归约为数字。但 Lean 在将定义编译为可执行代码时会擦除类型和命题信息,由于这些公理只添加新的命题,它们与这种计算解释是兼容的。即使是倾向于计算的用户也可能希望使用经典排中律(law of the excluded middle)来推理计算。这也会阻塞内核中的求值,但与编译后的代码兼容。

标准库还定义了一个选择原理(choice principle),它与计算解释完全背道而驰,因为它从断言存在的命题中神奇地产生“数据”。其使用对于某些经典构造是必不可少的,用户可以按需导入。但使用此构造生成数据的表达式不具有计算内容,在 Lean 中我们必须将这样的定义标记为noncomputable以标记这一事实。

利用一个巧妙的技巧(称为迪亚科内斯库定理(Diaconescu’s theorem)),可以使用命题外延性、函数外延性和选择原理推导出排中律。然而如上所述,排中律的使用以及其他的经典原理仍然与编译兼容,只要它们不被用来制造数据。

总结起来,在宇宙、依值函数类型和归纳类型的基础框架之上,标准库添加了三个额外的组件:

  • 命题外延性公理

  • 一个商构造,它蕴涵函数外延性

  • 一个选择原理,它从存在命题中产生数据。

前两者会阻塞 Lean 内部的规范化(normalization),但与代码生成兼容,而第三个则不适合计算解释。我们将在下面更精确地阐述细节。

12.1. 历史与哲学背景🔗

在其历史的大部分时间里,数学本质上是计算的:几何学处理几何对象的构造,代数学关注方程组的算法求解,分析学提供了计算随时间演化的系统未来行为的方法。从“对每一个x,存在一个y使得……”这样的定理证明中,通常可以直接提取一个算法,用于在给定x时计算出这样的y

然而,在十九世纪,数学论证复杂性的增加促使数学家发展出新的推理风格,这些风格抑制了算法信息,并引用了对数学对象的描述,抽象掉了这些对象如何表示的细节。目标是获得强大的“概念性”理解而不陷入计算细节的泥潭,但这导致了在直接的计算解读下显然是错误的数学定理的产生。

今天仍然存在相当一致的共识,即计算对数学很重要。但对于如何最好地处理计算问题,存在不同的观点。从构造性(constructive)的观点来看,将数学与其计算根源分离开来是错误的;每一个有意义的数学定理都应该具有直接的计算解释。从经典(classical)的观点来看,更有效的方法是保持关注点分离(separation of concerns):我们可以使用一种语言和一套方法来编写计算机程序,同时保持使用非构造性(nonconstructive)理论和方法来推理它们的自由。Lean 被设计为支持这两种方法。核心库的部分是以构造性方式开发的,但系统也提供了进行经典数学推理的支持。

从计算的角度来看,依值类型理论中最纯粹的部分完全避免使用Prop。归纳类型和依值函数类型可以被视为数据类型,这些类型的项(terms)可以通过应用归约规则直到无法再应用任何规则来“求值”。原则上,任何类型为Nat的封闭项(即没有自由变量的项)应该求值为一个数字,如succ ( (succ zero))

引入证明无关的Prop并将定理标记为不可归约是迈向关注点分离的第一步。其意图是类型p : Prop的元素在计算中不应起作用,因此项prf : p的特定构造在这个意义上是“无关的”。仍然可以定义包含Prop类型元素的计算对象;关键是这些元素可以帮助我们推理计算的效果,但在从项中提取“代码”时可以忽略。然而,Prop类型的元素并非完全无害。它们包括任意类型α上的等式s = t : α,这样的等式可以用作强制类型转换(cast)来对项进行类型检查。下面我们将看到这样的强制转换如何阻塞系统中的计算。然而,在擦除命题内容、忽略中间类型约束并将项归约直至其达到范式(normal form)的求值方案下,计算仍然是可能的。这正是 Lean 的虚拟机(virtual machine)所做的。

采用证明无关的Prop之后,人们可能会认为使用排中律p ¬p(其中p是任意命题)是合理的。当然,这也会阻塞 CIC 规则下的计算,但不会阻止如上所述的可执行代码的生成。只有在关于选择(choice)的章节中讨论的选择原理才完全抹消了理论中证明无关部分与数据相关部分之间的区别。

12.2. 命题外延性(Propositional Extensionality)🔗

命题外延性即以下公理:

axiom propext {a b : Prop} : (a b) a = b

它断言当两个命题相互蕴涵时,它们实际上是相等的。这与集合论解释一致,其中a : Prop的任意元素要么是空集,要么是单点集{*}(对于某个特定的元素*)。该公理的效果是等价的命题可以在任何上下文中相互替换:

variable (a b c d e : Prop) theorem thm₁ (h : a b) : (c a d e) (c b d e) := propext h Iff.refl _ theorem thm₂ (p : Prop Prop) (h : a b) (h₁ : p a) : p b := propext h h₁

12.3. 函数外延性(Function Extensionality)🔗

与命题外延性类似,函数外延性断言类型(x : α) β x的任意两个函数,如果在所有输入上一致,则它们是相等的:

funext.{u, v} {α : Sort u} {β : α Sort v} {f g : (x : α) β x} (h : (x : α), f x = g x) : f = g

从经典的集合论视角来看,这正是两个函数相等的含义。这被称为函数的“外延(extensional)”观点。然而,从构造性视角来看,有时更自然的是将函数视为以某种显式方式呈现的算法或计算机程序。当然,两个计算机程序可以为每个输入计算出相同的答案,尽管它们在语法上相当不同。类似地,你可能会希望保持一种不强迫你将输入/输出行为相同的两个函数视为同一的函数观点。这被称为函数的“内涵(intensional)”观点。

实际上,函数外延性可以从商(quotients)的存在性推导出来,我们将在下一节中描述。因此,在 Lean 标准库中,funext从商构造中证明的

假设对于α : Type u,我们定义Set α:= α Prop来表示α的子集类型,本质上是将子集与谓词(predicates)等同起来。通过组合funextpropext,我们获得了这些集合的外延理论:

def Set (α : Type u) := α Prop namespace Set def mem (x : α) (a : Set α) := a x infix:50 (priority := high) "∈" => mem theorem setext {a b : Set α} (h : x, x a x b) : a = b := funext (fun x => propext (h x)) end Set

然后我们可以继续定义空集和集合交集等,并证明集合恒等式:

def empty : Set α := fun _ => False notation (priority := high) "∅" => empty def inter (a b : Set α) : Set α := fun x => x a x b infix:70 " ∩ " => inter theorem inter_self (a : Set α) : a a = a := setext fun unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x => Iff.intro (fun h, _ => h) (fun h => h, h) theorem inter_empty (a : Set α) : a = := setext fun _ => Iff.intro (fun _, h => h) (fun h => False.elim h) theorem empty_inter (a : Set α) : a = := setext fun _ => Iff.intro (fun h, _ => h) (fun h => False.elim h) theorem inter.comm (a b : Set α) : a b = b a := setext fun _ => Iff.intro (fun h₁, h₂ => h₂, h₁) (fun h₁, h₂ => h₂, h₁)

以下是一个示例,说明函数外延性如何阻塞 Lean 内核内的计算:

def f (x : Nat) := x def g (x : Nat) := 0 + x theorem f_eq_g : f = g := funext fun x => (Nat.zero_add x).symm def val : Nat := Eq.recOn (motive := fun _ _ => Nat) f_eq_g 0 -- does not reduce to 0 f_eq_g 0#reduce val
f_eq_g  0
-- evaluates to 0 0#eval val
0

首先,我们使用函数外延性证明两个函数fg相等,然后通过将类型中的f替换为g来强制转换0(类型为Nat)的类型。当然,这个强制转换是空洞的,因为Nat不依赖于f。但这足以造成损害:在系统的计算规则下,我们现在有一个类型为Nat的封闭项,它不会归约为一个数字。在这种情况下,我们可能想将表达式归约为0。但在非平凡的例子中,消除强制转换会改变项的类型,可能使周围的表达式类型不正确。然而,虚拟机可以毫无问题地将表达式求值为0。以下是一个类似的人为示例,展示了propext如何造成阻碍:

theorem tteq : (True True) = True := propext (Iff.intro (fun h, _ => h) (fun h => h, h)) def val : Nat := Eq.recOn (motive := fun _ _ => Nat) tteq 0 -- does not reduce to 0 tteq 0#reduce val
tteq  0
-- evaluates to 0 0#eval val
0

当前的研究计划,包括观察类型论(observational type theory)立方类型论(cubical type theory)的工作,旨在以允许对涉及函数外延性、商等的强制转换进行归约的方式来扩展类型论。但这些解决方案并不那么明确,Lean 底层演算的规则也不认可这样的归约。

然而从某种意义上说,强制转换并不改变表达式的含义。相反,它是推理表达式类型的一种机制。在适当的语义下,以保持其含义的方式归约项是合理的,忽略为使归约类型正确所需的中间簿记。在这种情况下,在Prop中添加新公理并不重要;通过证明无关性(proof irrelevance)Prop中的表达式不携带任何信息,可以被归约过程安全地忽略。

12.4. 商(Quotients)🔗

α为任意类型,rα上的一个等价关系。数学上通常形成“商”α / r,即α“模”r的元素的类型。在集合论中,可以将α / r视为αr的等价类集合。如果f : α β是任何尊重该等价关系的函数,即对于任意x y : αr x y蕴涵f x = f y,那么f可以“提升”为一个函数f’ : α / r β,定义为f’ x = f x,其中x是包含x的等价类。Lean 的标准库使用额外的常量扩展了构造演算,这些常量精确地执行这些构造,并将最后一个等式作为定义性归约规则(definitional reduction rule)安装。

在其最基本的形式中,商构造甚至不需要r是等价关系。以下常量内置于 Lean 中:

universe u v axiom Quot : {α : Sort u} (α α Prop) Sort u axiom Quot.mk : {α : Sort u} (r : α α Prop) α Quot r axiom Quot.ind : {α : Sort u} {r : α α Prop} {β : Quot r Prop}, ( a, β (Quot.mk r a)) (q : Quot r) β q axiom Quot.lift : {α : Sort u} {r : α α Prop} {β : Sort u} (f : α β) ( a b, r a b f a = f b) Quot r β

第一个常量根据任意二元关系r在类型α上形成一个类型Quot r。第二个将α映射到Quot α,因此如果r : α α Propa : α,则Quot.mk r aQuot r的一个元素。第三个原理Quot.ind说明Quot.mk r a的每个元素都是这种形式。至于Quot.lift,给定一个函数f : α β,如果hf尊重关系r的证明,那么Quot.lift f h就是Quot r上对应的函数。其思想是,对于α中的每个元素a,函数Quot.lift f hQuot.mk r a(包含ar-类)映射到f a,其中h表明这个函数是良定义的。实际上,计算原理被声明为一条归约规则,如下面的证明所示。

def mod7Rel (x y : Nat) : Prop := x % 7 = y % 7 -- the quotient type Quot mod7Rel : Type#check (Quot mod7Rel : Type)
Quot mod7Rel : Type
-- the class of numbers equivalent to 4 Quot.mk mod7Rel 4 : Quot mod7Rel#check (Quot.mk mod7Rel 4 : Quot mod7Rel)
Quot.mk mod7Rel 4 : Quot mod7Rel
def f (x : Nat) : Bool := x % 7 = 0 theorem f_respects (a b : Nat) (h : mod7Rel a b) : f a = f b := a:Natb:Nath:mod7Rel a bf a = f b a:Natb:Nath:a % 7 = b % 7a % 7 = 0 b % 7 = 0 All goals completed! 🐙 Quot.lift f f_respects : Quot mod7Rel Bool#check (Quot.lift f f_respects : Quot mod7Rel Bool)
Quot.lift f f_respects : Quot mod7Rel  Bool
-- the computation principle example (a : Nat) : Quot.lift f f_respects (Quot.mk mod7Rel a) = f a := rfl

这四个常量QuotQuot.mkQuot.indQuot.lift本身并不非常强大。你可以验证,如果我们简单地将Quot r取为α,并将Quot.lift取为恒等函数(忽略h),Quot.ind就被满足。因此,这四个常量不被视为额外的公理。

它们就像归纳定义的类型及其相关的构造子和递归子(recursors)一样,被视为逻辑框架的一部分。

使Quot构造成为真正的商的是以下额外的公理:

axiom Quot.sound : {α : Type u} {r : α α Prop} {a b : α}, r a b Quot.mk r a = Quot.mk r b

这个公理断言α中通过r相关联的任意两个元素在商中被等同起来。如果某个定理或定义使用了Quot.sound,它将会出现在#print axioms命令的输出中。

当然,商构造最常用于r是等价关系的情况。给定如上的r,如果我们根据规则r’ a b当且仅当Quot.mk r a = Quot.mk r b来定义r’,那么显然r’是一个等价关系。实际上,r’是函数fun a => Quot.mk r a核(kernel)。公理Quot.sound说明r a b蕴涵r’ a b。使用Quot.liftQuot.ind,我们可以证明r’是包含r的最小等价关系,即如果r’’是任何包含r的等价关系,那么r’ a b蕴涵r’’ a b。特别地,如果r一开始就是等价关系,那么对所有abr a b当且仅当r’ a b

为了支持这种常见用例,标准库定义了集合论(setoid)的概念,它就是一个带有相关等价关系的类型:

class Setoid (α : Sort u) where r : α α Prop iseqv : Equivalence r instance {α : Sort u} [Setoid α] : HasEquiv α := Setoid.r namespace Setoid variable {α : Sort u} [Setoid α] theorem refl (a : α) : a a := iseqv.refl a theorem symm {a b : α} (hab : a b) : b a := iseqv.symm hab theorem trans {a b c : α} (hab : a b) (hbc : b c) : a c := iseqv.trans hab hbc end Setoid

给定一个类型αα上的一个关系r以及r是等价关系的证明iseqv,我们可以定义Setoid类的一个实例。

def Quotient {α : Sort u} (s : Setoid α) := @Quot α Setoid.r

常量Quotient.mkQuotient.indQuotient.liftQuotient.sound不过是Quot对应元素的特化。类型类(type class)推理可以为类型α找到相关的 setoid,这一事实带来了许多好处。首先,我们可以使用记号a b(通过pprox输入)表示Setoid.r a b,其中Setoid实例在记号Setoid.r中是隐式的。我们可以使用泛型定理Setoid.reflSetoid.symmSetoid.trans来推理该关系。特别是在商中,我们可以使用定理Quotient.exact

Quotient.exact {α : Sort u} {s : Setoid α} {a b : α} : Quotient.mk s a = Quotient.mk s b a b

Quotient.sound一起,这意味着商中的元素恰好对应于α中元素的等价类。

回顾一下,在标准库中,α × β表示类型αβ的笛卡尔积。为了说明商的使用,让我们将类型α无序对定义为类型α × α的商。首先,定义相关的等价关系:

private def eqv (p₁ p₂ : α × α) : Prop := (p₁.1 = p₂.1 p₁.2 = p₂.2) (p₁.1 = p₂.2 p₁.2 = p₂.1) infix:50 " ~ " => eqv

下一步是证明eqv实际上是一个等价关系,即它是自反的、对称的和传递的。我们可以通过使用依值模式匹配(dependent pattern matching)进行情况分析,将假设分解成若干部分,然后再重新组合以产生结论,以一种方便且可读的方式来证明这三个事实。

private theorem eqv.refl (p : α × α) : p ~ p := Or.inl rfl, rfl private theorem eqv.symm : {p₁ p₂ : α × α}, p₁ ~ p₂ p₂ ~ p₁ | (a₁, a₂), (b₁, b₂), (Or.inl a₁b₁, a₂b₂) => Or.inl (α:Type u_1a₁:αa₂:αb₁:αb₂:αa₁b₁:(a₁, a₂).fst = (b₁, b₂).fsta₂b₂:(a₁, a₂).snd = (b₁, b₂).snd(b₁, b₂).fst = (a₁, a₂).fst (b₁, b₂).snd = (a₁, a₂).snd All goals completed! 🐙) | (a₁, a₂), (b₁, b₂), (Or.inr a₁b₂, a₂b₁) => Or.inr (α:Type u_1a₁:αa₂:αb₁:αb₂:αa₁b₂:(a₁, a₂).fst = (b₁, b₂).snda₂b₁:(a₁, a₂).snd = (b₁, b₂).fst(b₁, b₂).fst = (a₁, a₂).snd (b₁, b₂).snd = (a₁, a₂).fst All goals completed! 🐙) private theorem eqv.trans : {p₁ p₂ p₃ : α × α}, p₁ ~ p₂ p₂ ~ p₃ p₁ ~ p₃ | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl a₁b₁, a₂b₂, Or.inl b₁c₁, b₂c₂ => Or.inl (α:Type u_1a₁:αa₂:αb₁:αb₂:αc₁:αc₂:αa₁b₁:(a₁, a₂).fst = (b₁, b₂).fsta₂b₂:(a₁, a₂).snd = (b₁, b₂).sndb₁c₁:(b₁, b₂).fst = (c₁, c₂).fstb₂c₂:(b₁, b₂).snd = (c₁, c₂).snd(a₁, a₂).fst = (c₁, c₂).fst (a₁, a₂).snd = (c₁, c₂).snd All goals completed! 🐙) | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inl a₁b₁, a₂b₂, Or.inr b₁c₂, b₂c₁ => Or.inr (α:Type u_1a₁:αa₂:αb₁:αb₂:αc₁:αc₂:αa₁b₁:(a₁, a₂).fst = (b₁, b₂).fsta₂b₂:(a₁, a₂).snd = (b₁, b₂).sndb₁c₂:(b₁, b₂).fst = (c₁, c₂).sndb₂c₁:(b₁, b₂).snd = (c₁, c₂).fst(a₁, a₂).fst = (c₁, c₂).snd (a₁, a₂).snd = (c₁, c₂).fst All goals completed! 🐙) | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr a₁b₂, a₂b₁, Or.inl b₁c₁, b₂c₂ => Or.inr (α:Type u_1a₁:αa₂:αb₁:αb₂:αc₁:αc₂:αa₁b₂:(a₁, a₂).fst = (b₁, b₂).snda₂b₁:(a₁, a₂).snd = (b₁, b₂).fstb₁c₁:(b₁, b₂).fst = (c₁, c₂).fstb₂c₂:(b₁, b₂).snd = (c₁, c₂).snd(a₁, a₂).fst = (c₁, c₂).snd (a₁, a₂).snd = (c₁, c₂).fst All goals completed! 🐙) | (a₁, a₂), (b₁, b₂), (c₁, c₂), Or.inr a₁b₂, a₂b₁, Or.inr b₁c₂, b₂c₁ => Or.inl (α:Type u_1a₁:αa₂:αb₁:αb₂:αc₁:αc₂:αa₁b₂:(a₁, a₂).fst = (b₁, b₂).snda₂b₁:(a₁, a₂).snd = (b₁, b₂).fstb₁c₂:(b₁, b₂).fst = (c₁, c₂).sndb₂c₁:(b₁, b₂).snd = (c₁, c₂).fst(a₁, a₂).fst = (c₁, c₂).fst (a₁, a₂).snd = (c₁, c₂).snd All goals completed! 🐙) private theorem is_equivalence : Equivalence (@eqv α) := { refl := eqv.refl, symm := eqv.symm, trans := eqv.trans }

既然我们已经证明了eqv是一个等价关系,我们可以构造一个Setoid (α × α),并使用它来定义无序对的类型UProd α

instance uprodSetoid (α : Type u) : Setoid (α × α) where r := eqv iseqv := is_equivalence def UProd (α : Type u) : Type u := Quotient (uprodSetoid α) namespace UProd def mk {α : Type} (a₁ a₂ : α) : UProd α := Quotient.mk' (a₁, a₂) notation "{ " a₁ ", " a₂ " }" => mk a₁ a₂ end UProd

注意我们在局部将无序对记号{a₁, a₂}定义为Quotient.mk' (a₁, a₂)。这对于说明目的很有用,但通常这不是一个好主意,因为这个记号会遮蔽花括号的其他用途,例如记录(records)和集合。

我们可以使用Quot.sound轻松证明{a₁, a₂} = {a₂, a₁},因为(a₁, a₂) ~ (a₂, a₁)

theorem mk_eq_mk (a₁ a₂ : α) : {a₁, a₂} = {a₂, a₁} := Quot.sound (Or.inr rfl, rfl)

为了完成这个示例,给定a : αu : UProd α,我们定义命题au,该命题在a是无序对u的一个元素时成立。首先,我们在(有序)对上定义一个类似的命题mem_fnau;然后我们使用引理mem_respects证明mem_fn尊重等价关系eqv。这是 Lean 标准库中广泛使用的一种惯用法。

private def mem_fn (a : α) : α × α Prop | (a₁, a₂) => a = a₁ a = a₂ -- auxiliary lemma for proving mem_respects private theorem mem_swap {a : α} : {p : α × α}, mem_fn a p = mem_fn a (p.2, p.1) α:Type u_1a:αa₁:αa₂:αmem_fn a (a₁, a₂) = mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) α:Type u_1a:αa₁:αa₂:αmem_fn a (a₁, a₂) = mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) α:Type u_1a:αa₁:αa₂:αmem_fn a (a₁, a₂) mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) α:Type u_1a:αa₁:αa₂:αmem_fn a (a₁, a₂) mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst)α:Type u_1a:αa₁:αa₂:αmem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) mem_fn a (a₁, a₂) α:Type u_1a:αa₁:αa₂:αmem_fn a (a₁, a₂) mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) intro α:Type u_1a:αa₁:αa₂:αx✝:mem_fn a (a₁, a₂)h:a = a₁mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) All goals completed! 🐙 α:Type u_1a:αa₁:αa₂:αx✝:mem_fn a (a₁, a₂)h:a = a₂mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) All goals completed! 🐙 α:Type u_1a:αa₁:αa₂:αmem_fn a ((a₁, a₂).snd, (a₁, a₂).fst) mem_fn a (a₁, a₂) intro α:Type u_1a:αa₁:αa₂:αx✝:mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst)h:a = (a₁, a₂).sndmem_fn a (a₁, a₂) All goals completed! 🐙 α:Type u_1a:αa₁:αa₂:αx✝:mem_fn a ((a₁, a₂).snd, (a₁, a₂).fst)h:a = (a₁, a₂).fstmem_fn a (a₁, a₂) All goals completed! 🐙 private theorem mem_respects : {p₁ p₂ : α × α} (a : α) p₁ ~ p₂ mem_fn a p₁ = mem_fn a p₂ α:Type u_1a₁:αa₂:αb₁:αb₂:αa:αa₁b₁:(a₁, a₂).fst = (b₁, b₂).fsta₂b₂:(a₁, a₂).snd = (b₁, b₂).sndmem_fn a (a₁, a₂) = mem_fn a (b₁, b₂) α:Type u_1a₁:αa₂:αb₁:αb₂:αa:αa₁b₁:(a₁, a₂).fst = (b₁, b₂).fsta₂b₂:(a₁, a₂).snd = (b₁, b₂).sndmem_fn a (a₁, a₂) = mem_fn a (b₁, b₂) All goals completed! 🐙 α:Type u_1a₁:αa₂:αb₁:αb₂:αa:αa₁b₂:(a₁, a₂).fst = (b₁, b₂).snda₂b₁:(a₁, a₂).snd = (b₁, b₂).fstmem_fn a (a₁, a₂) = mem_fn a (b₁, b₂) α:Type u_1a₁:αa₂:αb₁:αb₂:αa:αa₁b₂:(a₁, a₂).fst = (b₁, b₂).snda₂b₁:(a₁, a₂).snd = (b₁, b₂).fstmem_fn a (a₁, a₂) = mem_fn a (b₁, b₂) α:Type u_1a₁:αa₂:αb₁:αb₂:αa:αa₁b₂:a₁ = b₂a₂b₁:a₂ = b₁mem_fn a (b₂, b₁) = mem_fn a (b₁, b₂) All goals completed! 🐙 def mem (a : α) (u : UProd α) : Prop := Quot.liftOn u (fun p => mem_fn a p) (fun p₁ p₂ e => mem_respects a e) infix:50 (priority := high) " ∈ " => mem theorem mem_mk_left (a b : α) : a {a, b} := Or.inl rfl theorem mem_mk_right (a b : α) : b {a, b} := Or.inr rfl theorem mem_or_mem_of_mem_mk {a b c : α} : c {a, b} c = a c = b := fun h => h

为方便起见,标准库还定义了用于提升二元函数的Quotient.lift₂,以及用于对两个变量进行归纳的Quotient.ind₂

在本节结束前,我们给出一些关于为什么商构造蕴涵函数外延性的提示。不难证明(x : α) β x上的外延相等是一个等价关系,因此我们可以考虑“模等价”的函数类型extfun α β。当然,函数应用尊重这种等价关系,即如果f₁等价于f₂,那么f₁ a等于f₂ a。因此函数应用产生了一个函数extfun_app : extfun α β (x : α) β x。但对每一个fextfun_app (.mk _ f)定义性地等于fun x => f x,而后者又定义性地等于f。因此,当f₁f₂外延相等时,我们有以下等式链:

example (f₁ f₂ : (x : α) β x) (h : x, f₁ x = f₂ x) := calc f₁ _ = extfun_app (.mk _ f₁) := rfl _ = extfun_app (.mk _ f₂) := α:Sort uβ:α Sort vf₁:(x : α) β xf₂:(x : α) β xh: (x : α), f₁ x = f₂ xextfun_app (Quot.mk (fun f g => (x : α), f x = g x) f₁) = extfun_app (Quot.mk (fun f g => (x : α), f x = g x) f₂) α:Sort uβ:α Sort vf₁:(x : α) β xf₂:(x : α) β xh: (x : α), f₁ x = f₂ x (x : α), f₁ x = f₂ x; All goals completed! 🐙 _ = f₂ := rfl

因此,f₁等于f₂

12.5. 选择(Choice)🔗

为了表述标准库中定义的最后一个公理,我们需要Nonempty类型,其定义如下:

class inductive Nonempty (α : Sort u) : Prop where | intro (val : α) : Nonempty α

因为Nonempty α的类型是Prop且其构造子包含数据,所以它只能消解(eliminate)到Prop。实际上,Nonempty α等价于 x : α, True

example (α : Type u) : Nonempty α unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x : α, True := Iff.intro (fun a => a, trivial) (fun a, unused variable `h` Note: This linter can be disabled with `set_option linter.unusedVariables false`h => a)

我们的选择公理现在简单地表达如下:

axiom choice {α : Sort u} : Nonempty α α

仅仅给定h断言α非空,choice h就神奇地产生了一个α的元素。当然,这阻塞了任何有意义的计算:根据Prop的解释,h完全不包含关于如何找到这样一个元素的信息。

这个公理位于Classical命名空间中,因此定理的全名是Classical.choice。选择原理等价于不定描述(indefinite description)原理,可以使用子类型(subtypes)表达如下:

noncomputable def indefiniteDescription {α : Sort u} (p : α Prop) (h : x, p x) : {x // p x} := choice <| let x, px := h; x, px

由于它依赖于choice,Lean 无法为indefiniteDescription生成可执行代码,因此要求我们将定义标记为noncomputable。同样在Classical命名空间中,函数choose和性质choose_spec分解了indefiniteDescription输出的两个部分:

variable {α : Sort u} {p : α Prop} noncomputable def choose (h : x, p x) : α := (indefiniteDescription p h).val theorem choose_spec (h : x, p x) : p (choose h) := (indefiniteDescription p h).property

choice原理也抹消了Nonempty性质与更具构造性的Inhabited性质之间的区别:

noncomputable def inhabited_of_nonempty (h : Nonempty α) : Inhabited α := choice (let a := h; a)

在下一节中,我们将看到propextfunextchoice三者一起蕴涵排中律和所有命题的可判定性(decidability)。使用这些,可以加强不定描述原理如下:

strongIndefiniteDescription {α : Sort u} (p : α Prop) (h : Nonempty α) : {x // ( (y : α), p y) p x}

假设环境类型α非空,strongIndefiniteDescription p在存在满足p的元素时产生一个这样的α的元素。该定义的数据部分传统上被称为希尔伯特的艾普西隆函数(Hilbert’s epsilon function)

epsilon {α : Sort u} [h : Nonempty α] (p : α Prop) : α
epsilon_spec {α : Sort u} {p : α Prop} (hex : (y : α), p y) : p (@epsilon _ (nonempty_of_exists hex) p)

12.6. 排中律(The Law of the Excluded Middle)🔗

排中律如下所示:

Classical.em : (p : Prop), p ¬p

迪亚科内斯库定理(Diaconescu’s theorem)表明选择公理足以推导出排中律。更精确地说,它表明排中律可以从Classical.choicepropextfunext推导出来。我们概述标准库中能找到的证明。

首先,我们导入必要的公理,并定义两个谓词UV

open Classical theorem em (p : Prop) : p ¬p := p:Propp ¬p p:PropU:Prop Prop := fun x => x = True pp ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pp ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)p ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)p ¬p

如果p为真,那么Prop的每个元素既在U中也在V中。如果p为假,那么U是单点集True,而V是单点集False

接下来,我们使用chooseUV中各选择一个元素:

p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUp ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVp ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUp ¬p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVp ¬p

UV各自是一个析取(disjunction),所以u_defv_def代表了四种情况。在其中一种情况下,u = Truev = False,而在所有其他情况下p为真。因此我们有:

have not_uv_or_p : u v p := p:Propp ¬p match u_def, v_def with p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVh:px✝:V vu v p All goals completed! 🐙 p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVx✝:U uh:pu v p All goals completed! 🐙 p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVhut:u = Truehvf:v = Falseu v p p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVhut:u = Truehvf:v = Falseu v All goals completed! 🐙

另一方面,如果p为真,那么通过函数外延性和命题外延性,UV相等。根据uv的定义,这意味着它们也相等。

have p_implies_uv : p u = v := fun hp => have hpred : U = V := funext fun x => have hl : (x = True p) (x = False p) := fun _ => Or.inr hp have hr : (x = False p) (x = True p) := fun _ => Or.inr hp show (x = True p) = (x = False p) from propext (Iff.intro hl hr) have h₀ : exU exV, @choose _ U exU = @choose _ V exV := p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVnot_uv_or_p:u v p := match u_def, v_def with | Or.inr h, x => Or.inr h | x, Or.inr h => Or.inr h | Or.inl hut, Or.inl hvf => Or.inl (of_eq_true (Eq.trans (congr (congrArg Ne hut) hvf) (Eq.trans (congrArg Not (Eq.trans eq_iff_iff._simp_1 (Eq.trans (iff_false True) not_true_eq_false))) not_false_eq_true)))hp:phpred:U = V := funext fun x => have hl := fun x_1 => Or.inr hp; have hr := fun x_1 => Or.inr hp; have this := propext { mp := hl, mpr := hr }; this (exU : x, U x) (exV : x, V x), choose exU = choose exV p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVnot_uv_or_p:u v p := match u_def, v_def with | Or.inr h, x => Or.inr h | x, Or.inr h => Or.inr h | Or.inl hut, Or.inl hvf => Or.inl (of_eq_true (Eq.trans (congr (congrArg Ne hut) hvf) (Eq.trans (congrArg Not (Eq.trans eq_iff_iff._simp_1 (Eq.trans (iff_false True) not_true_eq_false))) not_false_eq_true)))hp:phpred:U = V := funext fun x => have hl := fun x_1 => Or.inr hp; have hr := fun x_1 => Or.inr hp; have this := propext { mp := hl, mpr := hr }; this (exU exV : x, V x), choose exU = choose exV; p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVnot_uv_or_p:u v p := match u_def, v_def with | Or.inr h, x => Or.inr h | x, Or.inr h => Or.inr h | Or.inl hut, Or.inl hvf => Or.inl (of_eq_true (Eq.trans (congr (congrArg Ne hut) hvf) (Eq.trans (congrArg Not (Eq.trans eq_iff_iff._simp_1 (Eq.trans (iff_false True) not_true_eq_false))) not_false_eq_true)))hp:phpred:U = V := funext fun x => have hl := fun x_1 => Or.inr hp; have hr := fun x_1 => Or.inr hp; have this := propext { mp := hl, mpr := hr }; thisexU✝: x, V xexV✝: x, V xchoose exU✝ = choose exV✝; All goals completed! 🐙 show u = v from h₀ _ _

将上述两个事实放在一起就得到了期望的结论:

match not_uv_or_p with p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVnot_uv_or_p:u v p := match u_def, v_def with | Or.inr h, x => Or.inr h | x, Or.inr h => Or.inr h | Or.inl hut, Or.inl hvf => Or.inl (of_eq_true (Eq.trans (congr (congrArg Ne hut) hvf) (Eq.trans (congrArg Not (Eq.trans eq_iff_iff._simp_1 (Eq.trans (iff_false True) not_true_eq_false))) not_false_eq_true)))p_implies_uv:p u = v := fun hp => have hpred := funext fun x => have hl := fun x_1 => Or.inr hp; have hr := fun x_1 => Or.inr hp; have this := propext { mp := hl, mpr := hr }; this; have h₀ := Eq.mpr (id (congrArg (fun _a => (exU : x, _a x) (exV : x, V x), choose exU = choose exV) hpred)) fun exU exV => Eq.refl (choose exU); have this := h₀ exU exV; thishne:u vp ¬p All goals completed! 🐙 p:PropU:Prop Prop := fun x => x = True pV:Prop Prop := fun x => x = False pexU: x, U x := Exists.intro True (Or.inl rfl)exV: x, V x := Exists.intro False (Or.inl rfl)u:Prop := choose exUv:Prop := choose exVu_def:U u := choose_spec exUv_def:V v := choose_spec exVnot_uv_or_p:u v p := match u_def, v_def with | Or.inr h, x => Or.inr h | x, Or.inr h => Or.inr h | Or.inl hut, Or.inl hvf => Or.inl (of_eq_true (Eq.trans (congr (congrArg Ne hut) hvf) (Eq.trans (congrArg Not (Eq.trans eq_iff_iff._simp_1 (Eq.trans (iff_false True) not_true_eq_false))) not_false_eq_true)))p_implies_uv:p u = v := fun hp => have hpred := funext fun x => have hl := fun x_1 => Or.inr hp; have hr := fun x_1 => Or.inr hp; have this := propext { mp := hl, mpr := hr }; this; have h₀ := Eq.mpr (id (congrArg (fun _a => (exU : x, _a x) (exV : x, V x), choose exU = choose exV) hpred)) fun exU exV => Eq.refl (choose exU); have this := h₀ exU exV; thish:pp ¬p All goals completed! 🐙

排中律的推论包括双重否定消去(double-negation elimination)、按情况证明(proof by cases)和反证法(proof by contradiction),所有这些都在经典逻辑一节中描述。排中律和命题外延性蕴涵了命题完备性(propositional completeness):

open Classical theorem propComplete (a : Prop) : a = True a = False := match em a with | Or.inl ha => Or.inl (propext (Iff.intro (fun _ => True.intro) (fun _ => ha))) | Or.inr hn => Or.inr (propext (Iff.intro (fun h => hn h) (fun h => False.elim h)))

与选择原理一起,我们还得到了更强的原理——每个命题都是可判定的(decidable)。回顾一下,Decidable命题类定义如下:

class inductive Decidable (p : Prop) where | isFalse (h : ¬p) : Decidable p | isTrue (h : p) : Decidable p

与只能消解到Propp ¬ p不同,类型Decidable p等价于和类型(sum type)Sum p (¬ p),它可以消解到任意类型。正是这些数据被用来编写 if-then-else 表达式。

作为一个经典推理的例子,我们使用choose来证明如果f : α β是单射且α是被居留的(inhabited),那么f存在一个左逆。为了定义左逆linv,我们使用一个 if-then-else 表达式。回顾一下,if h : c then t else edite c (fun h : c => t) (fun h : ¬ c => e)的记法。在linv的定义中,选择被使用了两次:首先,用于表明( a : α, f a = b)是“可判定的”,然后选择一个a使得f a = b。注意propDecidable是一个作用域实例(scoped instance),通过open Classical命令激活。我们使用这个实例来证明if-then-else表达式的合法性。(另请参见可判定命题(Decidable Propositions)中的讨论。)

open Classical noncomputable def linv [Inhabited α] (f : α β) : β α := fun b : β => if ex : ( a : α, f a = b) then choose ex else default theorem linv_comp_self {f : α β} [Inhabited α] (inj : {a b}, f a = f b a = b) : linv f f = id := funext fun a => have ex : a₁ : α, f a₁ = f a := a, rfl have feq : f (choose ex) = f a := choose_spec ex calc linv f (f a) _ = choose ex := rfl _ = a := inj feq

从经典的观点来看,linv是一个函数。从构造性的观点来看,它是不可接受的;因为通常没有办法实现这样的函数,这个构造不具有信息性。