Theorem Proving in Lean 4

4. 量词与等式🔗

上一章介绍了构造涉及命题连接词的证明的方法。在本章中,我们将逻辑构造的范畴扩展到全称量词(universal quantifier)和存在量词(existential quantifier),以及等式关系(equality relation)。

4.1. 全称量词🔗

注意,如果α是任意类型,我们可以将p上的一个一元谓词(predicate)表示为类型α Prop的对象。在这种情况下,给定x : αp x表示断言px成立。类似地,对象r : α α Prop表示α上的二元关系(binary relation):给定x y : αr x y表示断言xy相关。

全称量词(universal quantifier), x : α, p x旨在表示断言“对于每一个x : αp x”成立。与命题连接词一样,在自然演绎(natural deduction)系统中,“forall”由引入规则(introduction rule)和消去规则(elimination rule)支配。非正式地说,引入规则指出:

给定p x的一个证明,在x : α是任意的上下文中,我们得到证明 x : α, p x

消去规则如下:

给定一个证明 x : α, p x和任意项t : α,我们得到p t的一个证明。

与蕴含(implication)的情况一样,命题即类型(propositions-as-types)的解释现在发挥了作用。回忆依值箭头类型(dependent arrow type)的引入和消去规则:

给定一个项t,其类型为β x,在x : α是任意的上下文中,我们有(fun x : α => t) : (x : α) t

消去规则如下:

给定一个项s : (x : α) β x和任意项t : α,我们有s t : β t

p x的类型为Prop时,如果我们用 x : α, p x替换(x : α) β x,我们可以将这些解读为构建涉及全称量词的证明的正确规则。

构造演算(Calculus of Constructions)因此以这种方式将依值箭头类型与全称量词表达式等同起来。如果p是任意表达式, x : α, p不过是(x : α) p的另一种符号表示,在p是命题的情况下,前者比后者更自然。通常,表达式p将依赖于x : α。回想一下,在普通函数空间的情况下,我们可以将α β解释为(x : α) β的特例,其中β不依赖于x。类似地,我们可以将命题之间的蕴含p q视为 x : p, q的特例,其中表达式q不依赖于x

以下是一个示例,展示了命题即类型(propositions-as-types)对应关系如何付诸实践。

example (α : Type) (p q : α Prop) : ( x : α, p x q x) y : α, p y := fun h : x : α, p x q x => fun y : α => show p y from (h y).left

作为记法约定,我们给予全称量词最宽的作用域,因此需要括号来将上例中的量词限制在假设范围内。证明 y : α, p y的标准方式是取一个任意的y,然后证明p y。这就是引入规则。现在,给定h的类型为 x : α, p x q x,表达式h y的类型为p yq y。这就是消去规则。取左合取(left conjunct)给出所需的结论,p y

记住,仅在约束变量重命名上不同的表达式被视为等价的。因此,例如,我们可以在假设和结论中使用相同的变量x,并在证明中通过不同的变量z来实例化它:

example (α : Type) (p q : α Prop) : ( x : α, p x q x) x : α, p x := fun h : x : α, p x q x => fun z : α => show p z from And.left (h z)

作为另一个例子,以下是我们如何表达一个关系r是传递的(transitive):

variable (α : Type) (r : α α Prop) variable (trans_r : x y z, r x y r y z r x z) variable (a b c : α) variable (hab : r a b) (hbc : r b c) trans_r : (x y z : α), r x y r y z r x z#check trans_r
trans_r :  (x y z : α), r x y  r y z  r x z
trans_r a b c : r a b r b c r a c#check trans_r a b c
trans_r a b c : r a b  r b c  r a c
trans_r a b c hab : r b c r a c#check trans_r a b c hab
trans_r a b c hab : r b c  r a c
trans_r a b c hab hbc : r a c#check trans_r a b c hab hbc
trans_r a b c hab hbc : r a c

思考一下这里发生的事情。当我们用值a b c实例化trans_r时,我们最终得到r a br b cr a c的一个证明。将其应用于“假设”hab : r a b,我们得到蕴含r b cr a c的一个证明。最后,将其应用于假设hbc产生结论r a c的一个证明。

在这种情况下,提供参数a b c可能会很繁琐,而它们可以从hab hbc中推断出来。因此,通常将这些参数设为隐式(implicit):

variable (α : Type) (r : α α Prop) variable (trans_r : {x y z}, r x y r y z r x z) variable (a b c : α) variable (hab : r a b) (hbc : r b c) trans_r : r ?m.4 ?m.5 r ?m.5 ?m.6 r ?m.4 ?m.6#check trans_r
trans_r : r ?m.4 ?m.5  r ?m.5 ?m.6  r ?m.4 ?m.6
trans_r hab : r b ?m.6 r a ?m.6#check trans_r hab
trans_r hab : r b ?m.6  r a ?m.6
trans_r hab hbc : r a c#check trans_r hab hbc
trans_r hab hbc : r a c

好处是我们可以简单地写trans_r hab hbc作为r a c的证明。缺点是在表达式trans_rtrans_r hab中,Lean没有足够的信息来推断参数的类型。第一个#check命令的输出是r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3,表明在这种情况下隐式参数是未指定的。

以下是一个示例,展示了我们如何使用等价关系(equivalence relation)进行基本推理:

variable (α : Type) (r : α α Prop) variable (refl_r : x, r x x) variable (symm_r : {x y}, r x y r y x) variable (trans_r : {x y z}, r x y r y z r x z) example (a b c d : α) (hab : r a b) (hcb : r c b) (hcd : r c d) : r a d := trans_r (trans_r hab (symm_r hcb)) hcd

为了熟悉使用全称量词,你应该尝试本节末尾的一些练习。

正是依值箭头类型(以及其中的全称量词)的 typing 规则将Prop与其他类型区分开来。假设我们有α : Sort iβ : Sort j,其中表达式β可能依赖于变量x : α。那么(x : α) βSort (imax i j)的一个元素,其中imax i jij的最大值(如果j不为0),否则为0

其思想如下。如果j不为0,那么(x : α) βSort (max i j)的一个元素。换句话说,从αβ的依值函数类型“存在于”索引为ij的最大值的宇宙中。然而,假设βSort 0,即Prop的一个元素。在这种情况下,(x : α) β也是Sort 0的一个元素,无论α存在于哪个类型宇宙中。换句话说,如果β是一个依赖于α的命题,那么 x : α, β也是一个命题。这反映了将Prop解释为命题(而非数据)的类型,也是使Prop成为非直谓的(impredicative)的原因。

术语“直谓的(predicative)”源于二十世纪之交的基础性发展,当时像庞加莱(Poincaré)和罗素(Russell)这样的逻辑学家将集合论悖论归咎于“恶性循环”——当我们通过对包含正在被定义的属性本身的集合进行量化来定义一个属性时,这种循环就会出现。注意,如果α是任意类型,我们可以构造所有α上谓词的类型α Prop(即“α的幂类型”)。Prop的非直谓性意味着我们可以形成对α Prop进行量化的命题。特别地,我们可以通过对α上所有谓词进行量化来定义α上的谓词,这正是曾被认为有问题的循环类型。

4.2. 等式🔗

现在让我们转向 Lean 库中定义的最基本关系之一,即等式关系(equality relation)。在关于归纳类型(inductive types)的章节中,我们将解释等式是如何从 Lean 逻辑框架的原语出发定义的。在此之前,我们在此解释如何使用它。

当然,等式的一个基本性质是它是一个等价关系(equivalence relation):

Eq.refl.{u_1} {α : Sort u_1} (a : α) : a = a#check Eq.refl
Eq.refl.{u_1} {α : Sort u_1} (a : α) : a = a
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a#check Eq.symm
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a
Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c#check Eq.trans
Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c

我们可以通过告诉 Lean 不要插入隐式参数(此处显示为元变量(metavariable))来使输出更易读。

universe u @Eq.refl : {α : Sort u} (a : α), a = a#check @Eq.refl.{u}
@Eq.refl :  {α : Sort u} (a : α), a = a
@Eq.symm : {α : Sort u} {a b : α}, a = b b = a#check @Eq.symm.{u}
@Eq.symm :  {α : Sort u} {a b : α}, a = b  b = a
@Eq.trans : {α : Sort u} {a b c : α}, a = b b = c a = c#check @Eq.trans.{u}
@Eq.trans :  {α : Sort u} {a b c : α}, a = b  b = c  a = c

标记.{u}告诉 Lean 在宇宙u处实例化常量。

因此,例如,我们可以将上一节的示例特化到等式关系:

variable (α : Type) (a b c d : α) variable (hab : a = b) (hcb : c = b) (hcd : c = d) example : a = d := Eq.trans (Eq.trans hab (Eq.symm hcb)) hcd

我们也可以使用投影记号(projection notation):

example : a = d := (hab.trans hcb.symm).trans hcd

自反性(Reflexivity)比它看起来更强大。回想一下,构造演算(Calculus of Constructions)中的项具有计算解释,逻辑框架将具有共同归约(reduct)的项视为相同。因此,一些非平凡等式可以通过自反性来证明:

variable (α β : Type) example (f : α β) (a : α) : (fun x => f x) a = f a := Eq.refl _ example (a : α) (b : β) : (a, b).1 = a := Eq.refl _ example : 2 + 3 = 5 := Eq.refl _

框架的这个特性如此重要,以至于库为rfl定义了一个表示法用于Eq.refl _

example (f : α β) (a : α) : (fun x => f x) a = f a := rfl example (a : α) (b : β) : (a, b).1 = a := rfl example : 2 + 3 = 5 := rfl

然而,等式远不止是一个等价关系。它有一个重要性质:每个断言都尊重等价关系,即我们可以替换相等的表达式而不改变真值。也就是说,给定h1 : a = bh2 : p a,我们可以使用替换(substitution)构造出p b的证明:Eq.subst h1 h2

example (α : Type) (a b : α) (p : α Prop) (h1 : a = b) (h2 : p a) : p b := Eq.subst h1 h2 example (α : Type) (a b : α) (p : α Prop) (h1 : a = b) (h2 : p a) : p b := h1 h2

第二种表示法中的三角形是一个宏,构建在Eq.substEq.symm之上,你可以通过输入\t来输入它。

规则Eq.subst用于定义以下辅助规则,它们执行更明确的替换。它们旨在处理 applicative 项,即形式为s t的项。具体来说,congrArg可用于替换参数,congrFun可用于替换被应用的项,而congr可同时替换两者。

variable (α : Type) variable (a b : α) variable (f g : α Nat) variable (h₁ : a = b) variable (h₂ : f = g) example : f a = f b := congrArg f h₁ example : f a = g a := congrFun h₂ a example : f a = g b := congr h₂ h₁

Lean 的库包含大量常见的恒等式,例如:

variable (a b c : Nat) example : a + 0 = a := Nat.add_zero a example : 0 + a = a := Nat.zero_add a example : a * 1 = a := Nat.mul_one a example : 1 * a = a := Nat.one_mul a example : a + b = b + a := Nat.add_comm a b example : a + b + c = a + (b + c) := Nat.add_assoc a b c example : a * b = b * a := Nat.mul_comm a b example : a * b * c = a * (b * c) := Nat.mul_assoc a b c example : a * (b + c) = a * b + a * c := Nat.mul_add a b c example : a * (b + c) = a * b + a * c := Nat.left_distrib a b c example : (a + b) * c = a * c + b * c := Nat.add_mul a b c example : (a + b) * c = a * c + b * c := Nat.right_distrib a b c

注意,Nat.mul_addNat.add_mul分别是Nat.left_distribNat.right_distrib的别名。以上性质是针对自然数(类型Nat)陈述的。

以下是一个自然数中结合了替换与结合律和分配律的计算示例。

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y := have h1 : (x + y) * (x + y) = (x + y) * x + (x + y) * y := Nat.mul_add (x + y) x y have h2 : (x + y) * (x + y) = x * x + y * x + (x * y + y * y) := (Nat.add_mul x y x) (Nat.add_mul x y y) h1 h2.trans (Nat.add_assoc (x * x + y * x) (x * y) (y * y)).symm

注意,Eq.subst的第二个隐式参数(提供替换发生的上下文)的类型为α Prop。因此推断这个谓词需要高阶统一(higher-order unification)的实例。一般而言,确定高阶统一子是否存在的问题是不可判定的,Lean最多只能提供不完善和近似的解决方案。因此,Eq.subst并不总能如你所愿。宏h e使用更有效的启发式方法来计算这个隐式参数,并且通常能在应用Eq.subst失败的情况下成功。

由于等式推理非常常见且重要,Lean 提供了多种机制来更有效地执行它。下一节提供了让你以更自然和清晰的方式编写计算证明的语法。但更重要的是,等式推理得到了项重写器(term rewriter)、化简器(simplifier)和其他形式自动化的支持。项重写器和化简器将在下一节中简要描述,然后在下一章中更详细地介绍。

4.3. 计算证明🔗

计算证明(calculational proof)只是一系列中间结果的链条,旨在通过诸如等式的传递性等基本原理组合起来。在 Lean 中,计算证明以关键字calc开始,语法如下:

calc
  <expr>_0  'op_1'  <expr>_1  ':='  <proof>_1
  '_'       'op_2'  <expr>_2  ':='  <proof>_2
  ...
  '_'       'op_n'  <expr>_n  ':='  <proof>_n

注意,calc中的所有关系具有相同的缩进。每个<proof>_i<expr>_{i-1} op_i <expr>_i的一个证明。

我们也可以在第一个关系(紧接在<expr>_0之后)中使用_,这对于对齐关系/证明对的序列很有用:

calc <expr>_0
    '_' 'op_1' <expr>_1 ':=' <proof>_1
    '_' 'op_2' <expr>_2 ':=' <proof>_2
    ...
    '_' 'op_n' <expr>_n ':=' <proof>_n

示例如下:

variable (a b c d e : Nat) theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = b := h1 _ = c + 1 := h2 _ = d + 1 := congrArg Nat.succ h3 _ = 1 + d := Nat.add_comm d 1 _ = e := Eq.symm h4

这种编写证明的风格在与simprw策略(tactic)结合使用时最为有效,这些策略将在下一章中更详细地讨论。例如,使用rw进行重写(rewrite),上述证明可以写成如下形式:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = b := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + da = b All goals completed! 🐙 _ = c + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + db = c + 1 All goals completed! 🐙 _ = d + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + dc + 1 = d + 1 All goals completed! 🐙 _ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + dd + 1 = 1 + d All goals completed! 🐙 _ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d1 + d = e All goals completed! 🐙

本质上,rw策略使用一个给定的等式(可以是假设、定理名称或复杂项)来“重写”目标。如果这样做将目标简化为恒等式t = t,该策略应用自反性来证明它。

重写可以顺序应用,因此上述证明可以缩短为:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = d + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + da = d + 1 All goals completed! 🐙 _ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + dd + 1 = 1 + d All goals completed! 🐙 _ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d1 + d = e All goals completed! 🐙

甚至更简洁:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + da = e All goals completed! 🐙

simp策略通过反复应用给定的恒等式,以任意顺序,在项中任何适用之处进行重写。它还使用先前已声明给系统的其他规则,并明智地应用交换律以避免循环。因此,我们也可以如下证明该定理:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + da = e All goals completed! 🐙

我们将在下一章讨论rwsimp的变体。

calc命令可以为任何支持某种形式传递性的关系配置。它甚至可以组合不同的关系。

variable (a b c d : Nat) example (h1 : a = b) (h2 : b c) (h3 : c + 1 < d) : a < d := calc a = b := h1 _ < b + 1 := Nat.lt_succ_self b _ c + 1 := Nat.succ_le_succ h2 _ < d := h3

你可以通过添加Trans类型类的新实例来“教会”calc新的传递性定理。类型类将在后面介绍,但以下小示例演示了如何使用新的Trans实例来扩展calc表示法。

def divides (x y : Nat) : Prop := k, k*x = y def divides_trans (h₁ : divides x y) (h₂ : divides y z) : divides x z := let k₁, d₁ := h₁ let k₂, d₂ := h₂ k₁ * k₂, x:Naty:Natz:Nath₁:divides x yh₂:divides y zk₁:Natd₁:k₁ * x = yk₂:Natd₂:k₂ * y = zk₁ * k₂ * x = z All goals completed! 🐙 def divides_mul (x : Nat) (k : Nat) : divides x (k*x) := k, rfl instance : Trans divides divides divides where trans := divides_trans example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) := calc divides x y := h₁ _ = z := h₂ divides _ (2*z) := divides_mul .. infix:50 " | " => divides example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) := calc x | y := h₁ _ = z := h₂ _ | 2*z := divides_mul ..

上面的例子也清楚地表明,即使你的关系没有中缀表示法,你也可以使用calc。Lean 已经包含了整除性的标准 Unicode 表示法(使用,可以通过输入\dvd\mid来输入),所以上面的例子使用了普通的竖线以避免冲突。在实践中,这不是一个好主意,因为它有可能与match ... with表达式中使用的 ASCII |混淆。

使用calc,我们可以用更自然和清晰的方式编写上一节的证明。

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := calc (x + y) * (x + y) = (x + y) * x + (x + y) * y := x:Naty:Nat(x + y) * (x + y) = (x + y) * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x + y) * y := x:Naty:Nat(x + y) * x + (x + y) * y = x * x + y * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x * y + y * y) := x:Naty:Natx * x + y * x + (x + y) * y = x * x + y * x + (x * y + y * y) All goals completed! 🐙 _ = x * x + y * x + x * y + y * y := x:Naty:Natx * x + y * x + (x * y + y * y) = x * x + y * x + x * y + y * y All goals completed! 🐙

这里值得考虑另一种calc表示法。当第一个表达式占用这么多空间时,在第一个关系中使用_可以自然地对齐所有关系:

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := calc (x + y) * (x + y) _ = (x + y) * x + (x + y) * y := x:Naty:Nat(x + y) * (x + y) = (x + y) * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x + y) * y := x:Naty:Nat(x + y) * x + (x + y) * y = x * x + y * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x * y + y * y) := x:Naty:Natx * x + y * x + (x + y) * y = x * x + y * x + (x * y + y * y) All goals completed! 🐙 _ = x * x + y * x + x * y + y * y := x:Naty:Natx * x + y * x + (x * y + y * y) = x * x + y * x + x * y + y * y All goals completed! 🐙

这里Nat.add_assoc之前的左箭头告诉重写使用相反方向的恒等式。(你可以通过输入\l或使用 ASCII 等价形式<-来输入它。)如果追求简洁,rwsimp都可以独立完成工作:

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := x:Naty:Nat(x + y) * (x + y) = x * x + y * x + x * y + y * y All goals completed! 🐙 example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := x:Naty:Nat(x + y) * (x + y) = x * x + y * x + x * y + y * y All goals completed! 🐙

4.4. 存在量词🔗

最后,考虑存在量词(existential quantifier),它可以写作exists x : α, p x x : α, p x。两个版本实际上都是更冗长表达式Exists (fun x : α => p x)的记法上的便捷缩写,它在 Lean 的库中定义。

正如你现在所期望的,库中包含了引入规则和消去规则。引入规则很直接:要证明 x : α, p x,只需提供一个合适的项tp t的一个证明。以下是一些示例:

example : x : Nat, x > 0 := have h : 1 > 0 := Nat.zero_lt_succ 0 Exists.intro 1 h example (x : Nat) (h : x > 0) : y, y < x := Exists.intro 0 h example (x y z : Nat) (hxy : x < y) (hyz : y < z) : w, x < w w < z := Exists.intro y (And.intro hxy hyz) @Exists.intro : {α : Sort u_1} {p : α Prop} (w : α), p w Exists p#check @Exists.intro
@Exists.intro :  {α : Sort u_1} {p : α  Prop} (w : α), p w  Exists p

当类型在上下文中明确时,我们可以对Exists.intro t h使用匿名构造子(anonymous constructor)表示法t, h

example : x : Nat, x > 0 := have h : 1 > 0 := Nat.zero_lt_succ 0 1, h example (x : Nat) (h : x > 0) : y, y < x := 0, h example (x y z : Nat) (hxy : x < y) (hyz : y < z) : w, x < w w < z := y, hxy, hyz

注意,Exists.intro具有隐式参数:Lean 必须在结论 x, p x中推断出谓词p : α Prop。这并非易事。例如,如果我们有hg : g 0 0 = 0并写Exists.intro 0 hg,谓词p可能有许多可能的值,对应于定理 x, g x x = x x, g x x = 0 x, g x 0 = x等。Lean 使用上下文来推断哪一个合适。这在以下示例中进行了说明,其中我们设置选项pp.explicit为 true 来要求 Lean 的 pretty-printer 显示隐式参数。

variable (g : Nat Nat Nat) theorem gex1 (hg : g 0 0 = 0) : x, g x x = x := 0, hg theorem gex2 (hg : g 0 0 = 0) : x, g x 0 = x := 0, hg theorem gex3 (hg : g 0 0 = 0) : x, g 0 0 = x := 0, hg theorem gex4 (hg : g 0 0 = 0) : x, g x x = 0 := 0, hg set_option pp.explicit true -- display implicit arguments theorem gex1 : (g : Nat Nat Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) @Exists Nat fun x => @Eq Nat (g x x) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex1
theorem gex1 :  (g : Nat  Nat  Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) 
    @Exists Nat fun x => @Eq Nat (g x x) x :=
fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex2 : (g : Nat Nat Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) @Exists Nat fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex2
theorem gex2 :  (g : Nat  Nat  Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) 
    @Exists Nat fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x :=
fun g hg =>
  @Exists.intro Nat (fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x)
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex3 : (g : Nat Nat Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) @Exists Nat fun x => @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex3
theorem gex3 :  (g : Nat  Nat  Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) 
    @Exists Nat fun x =>
      @Eq Nat
        (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
          (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
        x :=
fun g hg =>
  @Exists.intro Nat
    (fun x =>
      @Eq Nat
        (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
          (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
        x)
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex4 : (g : Nat Nat Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) @Exists Nat fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex4
theorem gex4 :  (g : Nat  Nat  Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) 
    @Exists Nat fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) :=
fun g hg =>
  @Exists.intro Nat (fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg

我们可以将Exists.intro视为信息隐藏操作,因为它隐藏了断言语体中的见证(witness)。存在消去规则Exists.elim执行相反的操作。它允许我们通过展示q从对任意值wp w推出,来从 x : α, p x证明命题q。粗略地说,因为我们知道存在一个满足p xx,我们可以给它一个名字,比如说w。如果q没有提及w,那么展示qp w推出就相当于展示q从任何这样的x的存在性推出。示例如下:

variable (α : Type) (p q : α Prop) example (h : x, p x q x) : x, q x p x := Exists.elim h (fun w => fun hw : p w q w => show x, q x p x from w, hw.right, hw.left)

比较存在消去规则与析取消去规则可能会有所帮助:断言 x : α, p x可以被视为命题p a(其中a取遍α的所有元素)的一个大析取。注意,匿名构造子表示法w, hw.right, hw.left缩写了一个嵌套的构造子应用;我们也可以等价地写w, ⟨hw.right, hw.left⟩⟩

注意,存在命题与 Σ 类型非常相似,如依值类型部分所述。区别在于存在命题是命题,而 Σ 类型是类型。除此之外,它们非常相似。给定谓词p : α Prop和类型族β : α Type,对于项a : α,有h : p ah' : β a,项Exists.intro a h的类型为( x : α, p x) : Prop,而Sigma.mk a h'的类型为(Σ x : α, β x)Σ之间的相似性是柯里-霍华德同构(Curry-Howard isomorphism)的另一个实例。

Lean 提供了一种更方便的方式来使用match表达式从存在量词中消去:

variable (α : Type) (p q : α Prop) example (h : x, p x q x) : x, q x p x := match h with | w, hw => w, hw.right, hw.left

match表达式是 Lean 函数定义系统的一部分,它提供了定义复杂函数的便捷且富有表现力的方式。再次强调,正是柯里-霍华德同构(Curry-Howard isomorphism)允许我们也借用这种机制来编写证明。match语句将存在断言“析构”为分量whw,然后可以在语句体中使用它们来证明命题。我们可以注释 match 中使用的类型以获得更清晰:

example (h : x, p x q x) : x, q x p x := match h with | (w : α), (hw : p w q w) => w, hw.right, hw.left

我们甚至可以使用 match 语句同时分解合取:

example (h : x, p x q x) : x, q x p x := match h with | w, hpw, hqw => w, hqw, hpw

Lean 也提供了模式匹配的let表达式:

example (h : x, p x q x) : x, q x p x := let w, hpw, hqw := h w, hqw, hpw

这基本上只是上述match构造的另一种表示法。Lean 甚至允许我们在fun表达式中使用隐式match

example : ( x, p x q x) x, q x p x := fun w, hpw, hqw => w, hqw, hpw

我们将在归纳与递归(Induction and Recursion)中看到,所有这些变体都是一个更通用的模式匹配构造的实例。

在以下示例中,我们将IsEven a定义为 b, a = 2 * b,然后展示两个偶数之和是偶数。

def IsEven (a : Nat) := b, a = 2 * b theorem even_plus_even (h1 : IsEven a) (h2 : IsEven b) : IsEven (a + b) := Exists.elim h1 (fun w1 (hw1 : a = 2 * w1) => Exists.elim h2 (fun w2 (hw2 : b = 2 * w2) => Exists.intro (w1 + w2) (calc a + b _ = 2 * w1 + 2 * w2 := a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w2a + b = 2 * w1 + 2 * w2 All goals completed! 🐙 _ = 2 * (w1 + w2) := a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w22 * w1 + 2 * w2 = 2 * (w1 + w2) All goals completed! 🐙)))

使用本章中描述的各种工具——match 语句、匿名构造子和rewrite策略,我们可以简洁地编写这个证明如下:

theorem even_plus_even (h1 : IsEven a) (h2 : IsEven b) : IsEven (a + b) := match h1, h2 with | w1, hw1, w2, hw2 => w1 + w2, a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w2a + b = 2 * (w1 + w2) All goals completed! 🐙

正如构造性的“或”强于经典的“或”,构造性的“存在”也强于经典的“存在”。例如,以下蕴含需要经典推理,因为从构造性的角度来看,知道并非每个x都满足¬ p与拥有某个特定的x满足p是不同的。

open Classical variable (p : α Prop) example (h : ¬ x, ¬ p x) : x, p x := byContradiction (fun h1 : ¬ x, p x => have h2 : x, ¬ p x := fun x => fun h3 : p x => have h4 : x, p x := x, h3 show False from h1 h4 show False from h h2)

以下是一些涉及存在量词的常见恒等式。在下面的练习中,我们鼓励你尽可能多地证明它们。我们还留给你来确定哪些是非构造性的,因此需要某种形式的经典推理。

open Classical variable (α : Type) (p q : α Prop) variable (r : Prop) declaration uses 'sorry'example : ( x : α, r) r := sorry declaration uses 'sorry'example (a : α) : r ( x : α, r) := sorry declaration uses 'sorry'example : ( x, p x r) ( x, p x) r := sorry declaration uses 'sorry'example : ( x, p x q x) ( x, p x) ( x, q x) := sorry declaration uses 'sorry'example : ( x, p x) ¬ ( x, ¬ p x) := sorry declaration uses 'sorry'example : ( x, p x) ¬ ( x, ¬ p x) := sorry declaration uses 'sorry'example : (¬ x, p x) ( x, ¬ p x) := sorry declaration uses 'sorry'example : (¬ x, p x) ( x, ¬ p x) := sorry declaration uses 'sorry'example : ( x, p x r) ( x, p x) r := sorry declaration uses 'sorry'example (a : α) : ( x, p x r) ( x, p x) r := sorry declaration uses 'sorry'example (a : α) : ( x, r p x) (r x, p x) := sorry

注意,第二个示例和最后两个示例需要假设存在类型α的至少一个元素a

以下是其中两个较难问题的解答:

open Classical variable (α : Type) (p q : α Prop) variable (a : α) variable (r : Prop) example : ( x, p x q x) ( x, p x) ( x, q x) := Iff.intro (fun a, (h1 : p a q a) => Or.elim h1 (fun hpa : p a => Or.inl a, hpa) (fun hqa : q a => Or.inr a, hqa)) (fun h : ( x, p x) ( x, q x) => Or.elim h (fun a, hpa => a, (Or.inl hpa)) (fun a, hqa => a, (Or.inr hqa))) example : ( x, p x r) ( x, p x) r := Iff.intro (fun b, (hb : p b r) => fun h2 : x, p x => show r from hb (h2 b)) (fun h1 : ( x, p x) r => show x, p x r from byCases (fun hap : x, p x => a, λ unused variable `h'` Note: This linter can be disabled with `set_option linter.unusedVariables false`h' => h1 hap) (fun hnap : ¬ x, p x => byContradiction (fun hnex : ¬ x, p x r => have hap : x, p x := fun x => byContradiction (fun hnp : ¬ p x => have hex : x, p x r := x, (fun hp => absurd hp hnp) show False from hnex hex) show False from hnap hap)))

4.5. 更多关于证明语言🔗

我们已经看到,像funhaveshow这样的关键字使得编写反映非正式数学证明结构的正式证明项成为可能。在本节中,我们将讨论证明语言的一些其他常用特性。

首先,我们可以使用匿名have表达式来引入一个辅助目标而无需为其添加标签。我们可以使用关键字this来引用以这种方式引入的最后一个表达式:

variable (f : Nat Nat) variable (h : x : Nat, f x f (x + 1)) example : f 0 f 3 := have : f 0 f 1 := h 0 have : f 0 f 2 := Nat.le_trans this (h 1) show f 0 f 3 from Nat.le_trans this (h 2)

通常,证明从一个事实推进到下一个事实,因此这可以有效地消除大量标签的杂乱。

当目标可以被推断时,我们也可以要求 Lean 通过写by assumption来填充证明:

example : f 0 f 3 := have : f 0 f 1 := h 0 have : f 0 f 2 := Nat.le_trans (f:Nat Nath: (x : Nat), f x f (x + 1)this:f 0 f 1 := h 0f 0 f 1 All goals completed! 🐙) (h 1) show f 0 f 3 from Nat.le_trans (f:Nat Nath: (x : Nat), f x f (x + 1)this✝:f 0 f 1 := h 0this:f 0 f 2 := Nat.le_trans this✝ (h 1)f 0 f 2 All goals completed! 🐙) (h 2)

这告诉 Lean 使用assumption策略,该策略通过在局部上下文中找到合适的假设来证明目标。我们将在下一章中了解更多关于assumption策略的信息。

我们也可以要求 Lean 通过写p来填充证明,其中p是我们希望 Lean 在上下文中找到其证明的命题。你可以分别使用\f<\f>输入这些角引号。字母“f”代表“French(法语)”,因为这个 Unicode 符号也可以用作法语引号。事实上,这个表示法在 Lean 中定义如下:

notation "‹" p "›" => show p by assumption

这种方法比使用by assumption更稳健,因为需要推断的假设的类型被显式给出。它也使得证明更易读。以下是一个更详细的示例:

variable (f : Nat Nat) variable (h : x : Nat, f x f (x + 1)) example : f 0 f 1 f 1 f 2 f 0 = f 2 := fun _ : f 0 f 1 => fun _ : f 1 f 2 => have : f 0 f 2 := Nat.le_trans f 1 f 2 f 0 f 1 have : f 0 f 2 := Nat.le_trans (h 0) (h 1) show f 0 = f 2 from Nat.le_antisymm this f 0 f 2

请记住,你可以以这种方式使用法语引号来引用上下文中的任何内容,而不仅仅是匿名引入的东西。它的使用也不限于命题,尽管将其用于数据有些奇怪:

example (n : Nat) : Nat := Nat

稍后,我们将展示如何使用 Lean 宏系统扩展证明语言。

4.6. 练习

  1. 证明这些等价关系:

    variable (α : Type) (p q : α Prop) declaration uses 'sorry'example : ( x, p x q x) ( x, p x) ( x, q x) := sorry declaration uses 'sorry'example : ( x, p x q x) ( x, p x) ( x, q x) := sorry declaration uses 'sorry'example : ( x, p x) ( x, q x) x, p x q x := sorry

    你还应该尝试理解为什么最后一个示例中的反向蕴含是不可推导的。

  2. 通常可以将公式的某个分量移出全称量词,当该分量不依赖于被量化的变量时。尝试证明以下命题(其中第二个的有一个方向需要经典逻辑):

    variable (α : Type) (p q : α Prop) variable (r : Prop) declaration uses 'sorry'example : α (( x : α, r) r) := sorry declaration uses 'sorry'example : ( x, p x r) ( x, p x) r := sorry declaration uses 'sorry'example : ( x, r p x) (r x, p x) := sorry
  3. 考虑“理发师悖论(barber paradox)”,即声称某个城镇有一个(男)理发师为所有且只为那些不为自己刮胡子的男人刮胡子。证明这是一个矛盾:

    variable (men : Type) (barber : men) variable (shaves : men men Prop) declaration uses 'sorry'example (h : x : men, shaves barber x ¬ shaves x x) : False := sorry
  4. 记住,没有任何参数时,类型Prop的表达式只是一个断言。在下面填写primeFermat_prime的定义,并构造每个给定的断言。例如,你可以通过断言对于每个自然数n,存在一个大于n的素数来说明存在无限多个素数。哥德巴赫弱猜想(Goldbach's weak conjecture)指出每个大于 5 的奇数都可以表示为三个素数之和。如有必要,请查阅费马素数(Fermat prime)或其他陈述的定义。

    def declaration uses 'sorry'even (n : Nat) : Prop := sorry def declaration uses 'sorry'prime (n : Nat) : Prop := sorry def declaration uses 'sorry'infinitely_many_primes : Prop := sorry def declaration uses 'sorry'Fermat_prime (n : Nat) : Prop := sorry def declaration uses 'sorry'infinitely_many_Fermat_primes : Prop := sorry def declaration uses 'sorry'goldbach_conjecture : Prop := sorry def declaration uses 'sorry'Goldbach's_weak_conjecture : Prop := sorry def declaration uses 'sorry'Fermat's_last_theorem : Prop := sorry
  5. 尽可能多地证明“存在量词”部分列出的恒等式。