4. 量词与相等
上一章介绍了构造包含命题连接词的证明方法。在本章中, 我们扩展逻辑结构,包括全称量词和存在量词,以及相等关系。
4.1. 全称量词
如果 α 是任何类型,我们可以将 α 上的一元谓词 p 作为 α → Prop 类型的对象。在这种情况下,给定 x : α, p x 表示断言 p 在 x 上成立。类似地,一个对象 r : α → α → Prop 表示 α 上的二元关系:给定 x y : α,r x y 表示断言 x 与 y 相关。
全称量词 ∀ x : α, p x 表示,对于每一个 x : α,p x 成立。与命题连接词一样,在自然演绎系统中,「forall」有引入和消去规则。非正式地,引入规则是:
在
x : α是任意的情况下,给定p x的证明,就可以得到∀ x : α, p x的证明。
消去规则是:
给定
∀ x : α, p x的证明和任何项t : α,就可以得到p t的证明。
与蕴含的情况一样,命题即类型。回想依值箭头类型的引入规则:
在
x : α是任意的情况下,给定类型为β x的项t,就可以得到(fun x : α => t) : (x : α) → β x。
消去规则:
给定项
s : (x : α) → β x和任何项t : α,就可以得到s t : β t。
在 p x 具有 Prop 类型的情况下,如果我们用 ∀ x : α, p x 替换 (x : α) → β x,就得到构建涉及全称量词的证明的规则。
因此,构造演算用全称表达式来识别依值箭头类型。如果 p 是任何表达式,∀ x : α, p 不过是 (x : α) → p 的替代符号,在 p 是命题的情况下,前者比后者更自然。通常,表达式 p 取决于 x : α。回想一下,在普通函数空间中,我们可以将 α → β 解释为 (x : α) → β 的特殊情况,其中 β 不依赖于 x。类似地,我们可以把命题之间的蕴涵 p → q 看作是 ∀ x : p, q 的特殊情况,其中 q 不依赖于 x。
下面是一个例子,说明了如何运用 命题即类型 对应规则。
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
作为记法约定,我们给全称量词尽可能大的作用域,因此在上面的例子中需要括号来将 x 的量词限制在假设中。证明 ∀ y : α, p y 的规范方法是取一个任意的 y 并证明 p y。这就是引入规则。现在,给定 h 的类型为 ∀ x : α, p x ∧ q x,表达式 h y 的类型为 p y ∧ q y。这就是消去规则。取左结合项得到了所需的结论 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 是传递的:
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)
#check trans_r
#check trans_r a b c
#check trans_r a b c hab
#check trans_r a b c hab hbc
思考一下这里发生了什么。当我们用值 a b c 实例化 trans_r 时,我们最终得到了 r a b → r b c → r a c 的证明。将其应用于「假设」hab : r a b,我们得到了蕴涵式 r b c → r a c 的证明。最后,将其应用于假设 hbc 得到了结论 r a c 的证明。
在这种情况下,当可以从 hab hbc 推断出参数 a b c 时,提供这些参数可能会很繁琐。因此,通常将这些参数设为隐式:
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)
#check trans_r
#check trans_r hab
#check trans_r hab hbc
这样做的好处是我们可以简单地将 trans_r hab hbc 写成 r a c 的证明。缺点是 Lean 没有足够的信息来推断表达式 trans_r 和 trans_r hab 中参数的类型。第一个 #check 命令的输出是 r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3,表示在这种情况下隐式参数是未指定的。
下面是一个我们如何使用等价关系进行基本推理的例子:
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
为了习惯使用全称量词,你应该尝试本节末尾的一些练习。
正是依值箭头类型的类型规则,特别是全称量词,将 Prop 与其他类型区分开来。假设我们有 α : Sort i 和 β : Sort j,其中表达式 β 可能取决于变量 x : α。那么 (x : α) → β 是 Sort (imax i j) 的一个元素,其中 imax i j 是 i 和 j 的最大值(如果 j 不是 0),否则为 0。
其思想如下。如果 j 不是 0,那么 (x : α) → β 是 Sort (max i j) 的一个元素。换句话说,从 α 到 β 的依值函数类型「生活」在索引为 i 和 j 的最大值的宇宙中。然而,假设 β 是 Sort 0 的,即 Prop 的一个元素。在这种情况下,无论 α 生活在哪个类型宇宙中,(x : α) → β 也是 Sort 0 的一个元素。换句话说,如果 β 是取决于 α 的命题,那么 ∀ x : α, β 仍然是一个命题。这反映了将 Prop 解释为命题类型而非数据类型,这也是使 Prop 具有 非直谓性(impredicative) 的原因。
术语「谓词性(predicative)」源于 20 世纪初的基础发展,当时庞加莱(Poincaré)和罗素(Russell)等逻辑学家将集合论悖论归咎于「恶性循环」,即当我们通过对包含正在定义的属性本身的集合进行量化来定义该属性时,就会产生这种循环。请注意,如果 α 是任何类型,我们可以形成 α 上所有谓词的类型 α → Prop(α 的「幂类型」)。Prop 的非直谓性意味着我们可以形成对 α → Prop 进行量化的命题。特别是,
我们可以通过对 α 上的所有谓词进行量化来定义 α 上的谓词,这正是曾经被认为有问题的那种循环。
4.2. 相等
现在让我们转向 Lean 库中定义的最基本的关系之一,即相等关系。在关于 归纳类型 的章节中,我们将解释如何从 Lean 逻辑框架的原语中定义相等。与此同时,在这里我们解释如何使用它。
当然,相等的一个基本性质是它是一个等价关系:
我们可以通过告诉 Lean 不要插入隐式参数(在这里显示为元变量)来使输出更容易阅读。
标记 .{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
我们也可以使用投影记法:
自反性比看起来更强大。回想一下,构造演算中的项具有计算解释,并且逻辑框架将具有共同归约结果的项视为相同。因此,一些非平凡的恒等式可以通过自反性来证明:
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 _
框架的这一特性非常重要,以至于库为 Eq.refl _ 定义了一个记法 rfl:
variable (α β : Type)
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 = b 和 h2 : p a,我们可以使用替换构造 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.subst 和 Eq.symm 之上的宏,你可以通过输入 \t 来输入它。
规则 Eq.subst 用于定义以下辅助规则,这些规则执行更显式的替换。它们旨在处理应用项,即形式为 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_add 和 Nat.add_mul 分别是 Nat.left_distrib 和 Nat.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 提供了许多机制来更有效地执行它。下一节提供的语法允许你以更自然、更清晰的方式编写计算证明。但更重要的是,等式推理得到了项重写器、简化器和其他自动化工具的支持。项重写器和简化器将在下一节中简要介绍,然后在下一章中进行更详细的介绍。
4.3. 计算证明
计算证明只是一系列中间结果,这些结果旨在通过基本原理(如相等的传递性)组合而成。在 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
这种编写证明的风格在与 simp 和 rw 策略结合使用时最有效,这些策略将在下一章中更详细地讨论。例如,使用 rw(表示重写),上面的证明可以写成如下形式:
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 + d⊢ a = b All goals completed! 🐙
_ = c + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ b = c + 1 All goals completed! 🐙
_ = d + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ c + 1 = d + 1 All goals completed! 🐙
_ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ d + 1 = 1 + d All goals completed! 🐙
_ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ 1 + 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 + d⊢ a = d + 1 All goals completed! 🐙
_ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ d + 1 = 1 + d All goals completed! 🐙
_ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ 1 + 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 + d⊢ a = 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 + d⊢ a = e All goals completed! 🐙
我们将在下一章讨论 rw 和 simp 的变体。
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 = z⊢ k₁ * 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:Nat⊢ x * 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:Nat⊢ x * 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:Nat⊢ x * 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:Nat⊢ x * x + y * x + (x * y + y * y) = x * x + y * x + x * y + y * y All goals completed! 🐙
这里 Nat.add_assoc 前面的左箭头告诉重写以相反的方向使用恒等式。(你可以通过 \l 输入它,或者使用 ASCII 等效项 <-。)如果我们追求简洁,rw 和 simp 都可以独立完成工作:
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. 存在量词
最后,考虑存在量词,它可以写成 exists x : α, p x 或 ∃ x : α, p x。这两个版本实际上都是 Lean 库中定义的更冗长的表达式 Exists (fun x : α => p x) 的记法上的便利缩写。
正如你现在所期望的,库中既包含引入规则也包含消去规则。引入规则很简单:要证明 ∃ x : α, p x,只需提供一个合适的项 t 和 p 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)
#check @Exists.intro
当类型从上下文中很清楚时,我们可以为 Exists.intro t h 使用匿名构造器记法 ⟨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 的漂亮打印机显示隐式参数。
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
#print gex1
#print gex2
#print gex3
#print gex4
我们可以将 Exists.intro 视为一种信息隐藏操作,因为它隐藏了断言主体的见证。存在消去规则 Exists.elim 执行相反的操作。它允许我们通过证明 q 从任意值 w 的 p w 推导出来,从而从 ∃ x : α, p x 证明命题 q。粗略地说,既然我们知道存在一个满足 p x 的 x,我们可以给它起个名字,比如 w。如果 q 不涉及 w,那么证明 q 从 p 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⟩⟩。
注意,存在命题与依值类型一节中描述的 Sigma 类型非常相似。区别在于存在命题是 命题(propositions),而 Sigma 类型是 类型(types)。除此之外,它们非常相似。给定谓词 p : α → Prop 和类型族 β : α → Type,对于具有 h : p a 和 h' : β a 的项 a : α,项 Exists.intro a h 的类型为 (∃ x : α, p x) : Prop,而 Sigma.mk a h' 的类型为 (Σ x : α, β x)。∃ 和 Σ 之间的相似性是 Curry-Howard 同构 的另一个例子。
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 同构 允许我们也将这种机制用于编写证明。match 语句将存在断言「解构」为组件 w 和 hw,然后可以在语句主体中使用它们来证明命题。为了更清晰起见,我们可以对 match 中使用的类型进行注释:
variable (α : Type) (p q : α → Prop)
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 语句同时分解合取式:
variable (α : Type) (p q : α → Prop)
example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
match h with
| ⟨w, hpw, hqw⟩ => ⟨w, hqw, hpw⟩
Lean 还提供了一个模式匹配 let 表达式:
variable (α : Type) (p q : α → Prop)
example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x :=
let ⟨w, hpw, hqw⟩ := h
⟨w, hqw, hpw⟩
这本质上只是上述 match 结构的替代记法。Lean 甚至允许我们在 fun 表达式中使用隐式 match:
variable (α : Type) (p q : α → Prop)
example : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x :=
fun ⟨w, hpw, hqw⟩ => ⟨w, hqw, hpw⟩
我们将在 归纳与递归 中看到,所有这些变体都是更通用的模式匹配结构的实例。
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 * w2⊢ a + 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 * w2⊢ 2 * w1 + 2 * w2 = 2 * (w1 + w2) All goals completed! 🐙)))
在下面的例子中,我们将 IsEven a 定义为 ∃ b, a = 2 * b,然后证明两个偶数之和是一个偶数。
使用本章中描述的各种工具:match 语句、匿名构造器和 rewrite 策略,我们可以简洁地编写此证明如下:
def IsEven (a : Nat) := ∃ b, a = 2 * b
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 * w2⊢ a + b = 2 * (w1 + w2) All goals completed! 🐙⟩
正如构造性的「或」比经典逻辑的「或」更强一样,构造性的「存在」也比经典逻辑的「存在」更强。例如,下面的蕴涵式需要经典推理,因为从构造性的观点来看,知道并非每个 x 都满足 ¬ p 并不等同于拥有一个满足 p 的特定 x。
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)
example : (∃ x : α, r) → r := sorry
example (a : α) : r → (∃ x : α, r) := sorry
example : (∃ x, p x ∧ r) ↔ (∃ x, p x) ∧ r := sorry
example : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) := sorry
example : (∀ x, p x) ↔ ¬ (∃ x, ¬ p x) := sorry
example : (∃ x, p x) ↔ ¬ (∀ x, ¬ p x) := sorry
example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := sorry
example : (¬ ∀ x, p x) ↔ (∃ x, ¬ p x) := sorry
example : (∀ x, p x → r) ↔ (∃ x, p x) → r := sorry
example (a : α) : (∃ x, p x → r) ↔ (∀ x, p x) → r := 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, λ 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. 更多关于证明语言的内容
我们已经看到,像 fun、have 和 show 这样的关键字使得编写反映非正式数学证明结构的正式证明项成为可能。在本节中,我们将讨论证明语言的一些通常很方便的其他特性。
首先,我们可以使用匿名 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 0⊢ f 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 策略。
我们还可以通过编写 ‹p› 来要求 Lean 填充证明,其中 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›
请记住,你可以以这种方式使用法语引号来引用上下文中的 任何内容,而不仅仅是匿名引入的内容。它的用途也不限于命题,尽管将其用于数据有些奇怪:
稍后,我们将展示如何使用 Lean 宏系统扩展证明语言。
4.6. 练习
-
证明这些等价式:
variable (α : Type) (p q : α → Prop)example : (∀ x, p x ∧ q x) ↔ (∀ x, p x) ∧ (∀ x, q x) := sorryexample : (∀ x, p x → q x) → (∀ x, p x) → (∀ x, q x) := sorryexample : (∀ x, p x) ∨ (∀ x, q x) → ∀ x, p x ∨ q x := sorry
你还应该试着理解为什么在最后一个例子中反向蕴涵是不可导出的。
-
当公式的一个组件不依赖于量化变量时,通常可以将其移到全称量词之外。尝试证明这些(其中第二个的一个方向需要经典逻辑):
-
考虑「理发师悖论」,即声称在某个小镇上有一个(男)理发师,他只给所有不给自己刮胡子的男人刮胡子。证明这是一个矛盾:
-
记住,在没有任何参数的情况下,
Prop类型的表达式只是一个断言。填写下面prime和Fermat_prime的定义,并构造每个给定的断言。例如,你可以通过断言对于每个自然数n,都存在一个大于n的素数来表示有无穷多个素数。哥德巴赫弱猜想(Goldbach's weak conjecture)指出,每个大于 5 的奇数都是三个素数之和。如果有必要,请查阅费马素数(Fermat prime)的定义或任何其他陈述。def even (n : Nat) : Prop := sorrydef prime (n : Nat) : Prop := sorrydef infinitely_many_primes : Prop := sorrydef Fermat_prime (n : Nat) : Prop := sorrydef infinitely_many_Fermat_primes : Prop := sorrydef goldbach_conjecture : Prop := sorrydef Goldbach's_weak_conjecture : Prop := sorrydef Fermat's_last_theorem : Prop := sorry
-
尽可能多地证明「存在量词」一节中列出的恒等式。