Theorem Proving in Lean 4

3. 命题与证明🔗

到目前为止,你已经看到了一些在 Lean 中定义对象和函数的方法。在本章中,我们将开始解释如何用依值类型论(dependent type theory)的语言来书写数学断言和证明。

3.1. 命题即类型(Propositions as Types)🔗

在依值类型论语言中证明关于已定义对象的断言的一种策略,是在定义语言之上叠加一个断言语言和一个证明语言。但没有理由以这种方式增加语言的层次:依值类型论灵活且富有表现力,我们没有理由不能在同一个通用框架中表示断言和证明。

例如,我们可以引入一个新类型 Prop(命题宇宙)来表示命题,并引入构造子来从其他命题构建新命题。

And (a b : Prop) : Prop#check And
And (a b : Prop) : Prop
Or (a b : Prop) : Prop#check Or
Or (a b : Prop) : Prop
Not (a : Prop) : Prop#check Not
Not (a : Prop) : Prop
Implies (p q : Prop) : Prop#check Implies
Implies (p q : Prop) : Prop
variable (p q r : Prop) p q : Prop#check And p q
p  q : Prop
p q r : Prop#check Or (And p q) r
p  q  r : Prop
Implies (p q) (q p) : Prop#check Implies (And p q) (And q p)
Implies (p  q) (q  p) : Prop

然后,我们可以为每个 p : Prop 引入另一个类型 Proof p,作为 p 的证明类型。一个"公理(axiom)"就是这样一个类型的常量。

Proof (p : Prop) : Type#check Proof
Proof (p : Prop) : Type
axiom and_commut (p q : Prop) : Proof (Implies (And p q) (And q p)) variable (p q : Prop) and_commut p q : Proof (Implies (p q) (q p))#check and_commut p q
and_commut p q : Proof (Implies (p  q) (q  p))

然而,除了公理之外,我们还需要规则来从旧证明构建新证明。例如,在许多命题逻辑的证明系统中,我们有肯定前件(modus ponens)规则:

Implies p q 的一个证明和 p 的一个证明,我们得到 q 的一个证明。

我们可以如下表示:

axiom modus_ponens (p q : Prop) : Proof (Implies p q) Proof p Proof q

命题逻辑的自然 deduction(自然演绎)系统通常也依赖以下规则:

假设,在将 p 作为假设的前提下,我们有一个 q 的证明。那么我们可以"取消"这个假设,并得到 Implies p q 的一个证明。

我们可以如下呈现:

axiom implies_intro (p q : Prop) : (Proof p Proof q) Proof (Implies p q)

这种方法为我们提供了一种构建断言和证明的合理方式。判断一个表达式 t 是断言 p 的一个正确证明,就简化为检查 t 是否具有类型 Proof p

然而,可以进行一些简化。首先,我们可以通过将 Proof pp 本身合并来避免重复书写术语 Proof。换句话说,每当我们有 p : Prop 时,我们可以将 p 解释为一个类型,即其证明的类型。然后我们可以将 t : p 解读为断言 tp 的一个证明。

此外,一旦我们做出了这种等同,蕴含的规则表明我们可以在 Implies p qp q 之间来回转换。换句话说,命题 pq 之间的蕴含对应于存在一个函数,它将 p 的任何元素映射到 q 的一个元素。结果,连接词 Implies 的引入完全是多余的:我们可以使用依值类型论中通常的函数空间构造子 p q 作为我们的蕴含概念。

这就是构造演算(Calculus of Constructions)中所采用的方法,因此在 Lean 中也是如此。自然演绎证明系统中蕴含的规则恰好对应于支配函数抽象和应用的规则,这是 Curry-Howard 同构(Curry-Howard isomorphism)的一个实例,有时也被称为命题即类型(propositions-as-types)范式。实际上,类型 PropSort 0 的语法糖,也就是上一章描述的类型层次结构的最底层。此外,Type u 也只是 Sort (u+1) 的语法糖。Prop 有一些特殊特性,但和其他类型宇宙一样,它在箭头构造子下封闭:如果我们有 p q : Prop,那么 p q : Prop

至少有两种思考命题即类型的方式。对于某些持构造性逻辑和数学观点的人来说,这是对命题含义的忠实呈现:一个命题 p 代表一种数据类型,即构成证明的数据类型的规范。那么 p 的一个证明就是一个正确类型的对象 t : p

不倾向于这种意识形态的人,则可以将其视为一种简单的编码技巧。对于每个命题 p,我们关联一个类型,如果 p 为假则该类型为空,如果 p 为真则该类型只有一个元素,比如 *。在后一种情况下,我们说(与 p 关联的)类型是可居的(inhabited)。恰好,函数应用和抽象的规则可以方便地帮助我们跟踪 Prop 中哪些元素是可居的。因此,构造一个元素 t : p 告诉我们 p 确实是真。你可以将 p 的居留者视为"p 为真的事实"。p q 的一个证明利用"p 为真的事实"来得到"q 为真的事实"。

实际上,如果 p : Prop 是任意命题,Lean 的内核将任意两个元素 t1 t2 : p 视为定义上相等,就像它将 (fun x => t) st[s/x] 视为定义上相等一样。这被称为证明不相关性(proof irrelevance),并且与上一段的解释一致。这意味着,尽管我们可以将证明 t : p 视为依值类型论语言中的普通对象,但它们除了 p 为真这一事实之外,不携带任何其他信息。

我们建议的两种思考命题即类型(propositions-as-types)范式的方式存在根本性差异。从构造性观点来看,证明是由依值类型论中合适的表达式表示(denoted)的抽象数学对象。相比之下,如果我们按照上面描述的编码技巧来思考,那么这些表达式本身并不表示任何有趣的东西。相反,正是我们能够写出它们并检查它们是否良好类型化这一事实,确保了所讨论的命题为真。换句话说,这些表达式本身就是证明。

在下面的叙述中,我们将在两种说法之间来回切换,有时说一个表达式"构造"、"产生"或"返回"一个命题的证明,有时则简单地说它"是"这样一个证明。这类似于计算机科学家有时模糊语法和语义之间的区别,有时说一个程序"计算"某个函数,有时则把这个程序说成"是"该函数本身。

无论如何,真正重要的只有最终结果。为了在依值类型论的语言中形式化地表达一个数学断言,我们需要展示一个项 p : Prop。为了证明那个断言,我们需要展示一个项 t : p。Lean 作为证明助手的任务,就是帮助我们构造这样的项 t,并验证它是否良好形式化且具有正确的类型。

3.2. 以命题即类型的方式工作(Working with Propositions as Types)🔗

命题即类型(propositions-as-types)范式中,仅涉及 的定理可以使用 λ 抽象(lambda abstraction)和应用来证明。在 Lean 中,theorem(定理)命令引入一个新定理:

set_option linter.unusedVariables false --- variable {p : Prop} variable {q : Prop} theorem t1 : p q p := fun hp : p => fun hq : q => hp

将这个证明与表达式 fun x : α => fun y : β => x 进行比较,它的类型是 α β α,其中 αβ 是数据类型。这描述了一个函数,它接受类型分别为 αβ 的参数 xy,并返回 xt1 的证明具有相同的形式,唯一的区别是 pqProp 的元素而不是 Type 的元素。直观上,我们证明 p q p 时,假设 pq 为真,并使用第一个假设(平凡地)来确定结论 p 为真。

注意,theorem(定理)命令实际上是 def(定义)命令的一个版本:根据命题与类型的对应关系,证明定理 p q p 实际上与定义关联类型的一个元素是同一回事。对于内核类型检查器来说,两者之间没有区别。

然而,定义和定理之间存在一些实用上的差异。在正常情况下,永远不需要"展开"定理的"定义";根据证明不相关性(proof irrelevance),该定理的任意两个证明都是定义上相等的。一旦定理的证明完成,通常我们只需要知道证明存在即可;证明具体是什么并不重要。鉴于这一事实,Lean 将证明标记为不可归约的(irreducible),这向解析器(更准确地说是精化器(elaborator))提示在处理文件时通常不需要展开它们。实际上,Lean 通常能够并行处理和检查证明,因为评估一个证明的正确性不需要知道另一个证明的细节。此外,在定义的主体中引用的段变量(section variables)会自动添加为参数,但只有定理类型中引用的变量才会被添加。这是因为证明一个语句的方式不应影响被证明的语句。

与定义一样,#print(打印)命令会显示定理的证明:

theorem t1 : p q p := fun hp : p => fun hq : q => hp theorem t1 : {p q : Prop}, p q p := fun {p q} hp hq => hp#print t1
theorem t1 :  {p q : Prop}, p  q  p :=
fun {p q} hp hq => hp

注意,λ 抽象 hp : phq : q 可以被视为在 t1 的证明中的临时假设。Lean 还允许我们使用 show(显示)语句明确指定最终项 hp 的类型:

theorem t1 : p q p := fun hp : p => fun hq : q => show p from hp

添加这样的额外信息可以提高证明的清晰度,并有助于在编写证明时发现错误。show 命令除了注释类型之外不做任何事,并且在内部,我们所见过的所有 t1 的表示都产生相同的项。

与普通定义一样,我们可以将 λ 抽象的变量移到冒号的左边:

theorem t1 (hp : p) (hq : q) : p := hp theorem t1 : {p q : Prop}, p q p := fun {p q} hp hq => hp#print t1
theorem t1 :  {p q : Prop}, p  q  p :=
fun {p q} hp hq => hp

我们可以像函数应用一样使用定理 t1

theorem t1 (hp : p) (hq : q) : p := hp axiom hp : p theorem t2 : q p := t1 hp

axiom(公理)声明假定了给定类型的元素的存在,可能损害逻辑一致性。例如,我们可以用它来假定空类型 False 有一个元素:

axiom unsound : False -- Everything follows from false theorem ex : 1 = 0 := False.elim unsound

声明一个"公理" hp : p 相当于声明 p 为真,以 hp 为证。将定理 t1 : p q p 应用于事实 hp : p(即 p 为真)得到定理 t1 hp : q p

回顾一下,我们也可以如下编写定理 t1

theorem t1 {p q : Prop} (hp : p) (hq : q) : p := hp theorem t1 : {p q : Prop}, p q p := fun {p q} hp hq => hp#print t1
theorem t1 :  {p q : Prop}, p  q  p :=
fun {p q} hp hq => hp

t1 的类型现在是 {p q : Prop}, p q p。我们可以将其解读为断言"“对于每一对命题 p q,我们有 p q p”。例如,我们可以将所有参数移到冒号的右边:

theorem t1 : {p q : Prop}, p q p := fun {p q : Prop} (hp : p) (hq : q) => hp

如果 pq 已经被声明为变量(variables),Lean 会自动为我们泛化它们:

variable {p q : Prop} theorem t1 : p q p := fun (hp : p) (unused variable `hq` Note: This linter can be disabled with `set_option linter.unusedVariables false`hq : q) => hp

当我们这样泛化 t1 后,我们可以将其应用于不同的命题对,以获得一般定理的不同实例。

theorem t1 (p q : Prop) (hp : p) (hq : q) : p := hp variable (p q r s : Prop) t1 p q : p q p#check t1 p q
t1 p q : p  q  p
t1 r s : r s r#check t1 r s
t1 r s : r  s  r
t1 (r s) (s r) : (r s) (s r) r s#check t1 (r s) (s r)
t1 (r  s) (s  r) : (r  s)  (s  r)  r  s
variable (h : r s) t1 (r s) (s r) h : (s r) r s#check t1 (r s) (s r) h
t1 (r  s) (s  r) h : (s  r)  r  s

再次使用命题即类型(propositions-as-types)对应关系,类型为 h 的变量,其类型为 r s,可以被视为假设或前提,即 r s 成立。

作为另一个例子,让我们考虑上一章讨论的组合函数,现在使用命题而不是类型。

variable (p q r s : Prop) theorem t2 (h₁ : q r) (h₂ : p q) : p r := fun h₃ : p => show r from h₁ (h₂ h₃)

作为命题逻辑的一个定理,t2 表达的是什么?

注意,使用数字 Unicode 下标通常很有用,就像我们在本例中做的那样,通过输入 \0\1\2……来作为假设。

3.3. 命题逻辑(Propositional Logic)🔗

Lean 定义了所有标准逻辑连接词及其符号。命题连接词带有如下符号:

ASCII

Unicode

编辑器快捷键

定义

True

True

False

False

Not

¬

\not\neg

Not

/\

\and

And

\/

\or

Or

->

\to\r\imp

<->

\iff\lr

Iff

它们都取值于 Prop

variable (p q : Prop) p q p q : Prop#check p q p q
p  q  p  q : Prop
¬p p False : Prop#check ¬p p False
¬p  p  False : Prop
p q q p : Prop#check p q q p
p  q  q  p : Prop

运算优先级如下:一元否定 ¬ 绑定最强,然后是 ,然后是 ,然后是 ,最后是 。例如,a b c d e 意思是 (a b) (c (d e))。请记住 是右结合的(现在参数是 Prop 的元素而不是其他 Type,这并没有改变),其他二元连接词也是如此。所以如果我们有 p q r : Prop,表达式 p q r 解读为"“如果 p,那么如果 q,则 r”。这正好是 p q r 的"“柯里化”形式。

在上一章中,我们观察到 λ 抽象可以被视为 的"“引入规则”。在当前设置中,它展示了如何"“引入”或确立一个蕴含。应用可以被视为"“消去规则”,展示了如何在证明中"“消去”或使用一个蕴含。其他命题连接词在 Lean 的库中定义,并自动导入。每个连接词都带有其规范的引入和消去规则。

3.3.1. 合取(Conjunction)🔗

表达式 And.intro h1 h2 使用证明 h1 : ph2 : q 构建 p q 的一个证明。通常将 And.intro 描述为且引入(and-introduction)规则。在下一个例子中,我们使用 And.intro 来创建 p q p q 的一个证明。

variable (p q : Prop) example (hp : p) (hq : q) : p q := And.intro hp hq fun hp hq => hp, hq : p q p q#check fun (hp : p) (hq : q) => And.intro hp hq
fun hp hq => hp, hq : p  q  p  q

example(示例)命令声明一个定理而不命名它,也不将其存储在永久上下文中。本质上,它只检查给定的项是否具有指定的类型。它便于演示,我们将经常使用它。

表达式 And.left h 从证明 h : p q 创建 p 的一个证明。类似地,And.right hq 的一个证明。它们通常被称为左且消去和右且消去(and-elimination)规则。

variable (p q : Prop) example (h : p q) : p := And.left h example (h : p q) : q := And.right h

我们现在可以用以下证明项来证明 p q q p

variable (p q : Prop) example (h : p q) : q p := And.intro (And.right h) (And.left h)

注意,且引入和且消去类似于笛卡尔积的配对和投影操作。区别在于,给定 hp : phq : qAnd.intro hp hq 的类型是 p q : Prop,而给定 a : αb : βProd.mk a b 的类型是 α × β : TypeProd 不能与 Prop 一起使用,而 And 不能与 Type 一起使用。× 之间的相似性是 Curry-Howard 同构的另一个实例,但与蕴含和函数空间构造子不同,× 在 Lean 中是分开处理的。然而,根据这个类比,我们刚刚构建的证明类似于交换一对元素的函数。

我们将在结构体与记录(Structures and Records)中看到,Lean 中的某些类型是结构体(structures),也就是说,该类型由一个单一的规范构造子(constructor)定义,该构造子从一系列合适的参数中构建类型的一个元素。对于每个 p q : Propp q 就是一个例子:构造元素的规范方式是向合适的参数 hp : phq : q 应用 And.intro。在这些情况下,当相关类型是归纳类型并且可以从上下文中推断出来时,Lean 允许我们使用匿名构造子(anonymous constructor)符号 ⟨arg1, arg2, ...⟩。特别是,我们经常可以写 hp, hq 而不是 And.intro hp hq

variable (p q : Prop) variable (hp : p) (hq : q) hp, hq : p q#check (hp, hq : p q)
hp, hq : p  q

这些尖括号分别通过输入 \<\> 得到。

Lean 提供了另一个有用的语法工具。给定一个归纳类型 Foo(可能应用于某些参数)的表达式 e,符号 e.barFoo.bar e 的简写。这提供了一种无需打开命名空间就能访问函数的便捷方式。例如,以下两个表达式意思相同:

因此,给定 h : p q,我们可以写 h.left 来代替 And.left h,写 h.right 来代替 And.right h。因此,我们可以方便地将上面的示例证明重写如下:

variable (p q : Prop) example (h : p q) : q p := h.right, h.left

简洁和晦涩之间只有一线之隔,以这种方式省略信息有时会使证明更难阅读。但对于像上面这样直接的构造,当 h 的类型和构造的目标都很明显时,这种符号既清晰又有效。

像"And"这样的构造经常被嵌套使用。Lean 也允许你展平右结合的嵌套构造子,因此这两个证明是等价的:

variable (p q : Prop) example (h : p q) : q p q := h.right, h.left, h.right example (h : p q) : q p q := h.right, h.left, h.right

这通常也很有用。

3.3.2. 析取(Disjunction)🔗

表达式 Or.intro_left q hp 从证明 hp : p 创建 p q 的一个证明。类似地,Or.intro_right p hq 使用证明 hq : q 创建 p q 的一个证明。这些是左或引入和右或引入(or-introduction)规则。

variable (p q : Prop) example (hp : p) : p q := Or.intro_left q hp example (hq : q) : p q := Or.intro_right p hq

或消去(or-elimination)规则稍微复杂一些。其思想是,我们可以通过证明 rp 下成立以及 rq 下成立来从 p q 证明 r。换句话说,这是一种情况分析证明。在表达式 Or.elim hpq hpr hqr 中,Or.elim 接受三个参数:hpq : p qhpr : p rhqr : q r,并产生 r 的一个证明。在下面的例子中,我们使用 Or.elim 来证明 p q q p

variable (p q r : Prop) example (h : p q) : q p := Or.elim h (fun hp : p => show q p from Or.intro_right q hp) (fun hq : q => show q p from Or.intro_left p hq)

在大多数情况下,Or.intro_rightOr.intro_left 的第一个参数可以由 Lean 自动推断。因此,Lean 提供了 Or.inrOr.inl,它们可以视为 Or.intro_right _Or.intro_left _ 的简写。因此,上面的证明项可以更简洁地写成:

variable (p q r : Prop) example (h : p q) : q p := Or.elim h (fun hp => Or.inr hp) (fun hq => Or.inl hq)

注意,完整表达式中有足够的信息让 Lean 推断出 hphq 的类型。但在更长的版本中使用类型注释可以使证明更具可读性,并有助于捕获和调试错误。

因为 Or 有两个构造子,我们不能使用匿名构造子符号。但我们仍然可以写 h.elim 来代替 Or.elim h

variable (p q r : Prop) example (h : p q) : q p := h.elim (fun hp => Or.inr hp) (fun hq => Or.inl hq)

再次强调,你应该自行判断这样的缩写是增强了可读性还是降低了可读性。

3.3.3. 否定与假(Negation and Falsity)🔗

否定 ¬p 实际上被定义为 p False,因此我们通过从 p 推导出矛盾来得到 ¬p。类似地,表达式 hnp hphp : phnp : ¬p 产生 False 的一个证明。下一个例子使用这两条规则来产生 (p q) ¬q ¬p 的一个证明。(符号 ¬ 通过输入 \not\neg 得到。)

variable (p q : Prop) example (hpq : p q) (hnq : ¬q) : ¬p := fun hp : p => show False from hnq (hpq hp)

连接词 False 有一个消去规则 False.elim,它表达了从矛盾可以推出任何命题这一事实。这条规则有时被称为ex falsoex falso sequitur quodlibet 的简称),或爆炸原理(principle of explosion)

variable (p q : Prop) example (hp : p) (hnp : ¬p) : q := False.elim (hnp hp)

从假推出的任意事实 qFalse.elim 中的一个隐式参数,会自动推断。这种从矛盾假设推导出任意事实的模式非常常见,由 absurd 表示。

variable (p q : Prop) example (hp : p) (hnp : ¬p) : q := absurd hp hnp

例如,以下是 ¬p q (q p) r 的一个证明:

variable (p q r : Prop) example (hnp : ¬p) (hq : q) (hqp : q p) : r := absurd (hqp hq) hnp

顺便提一下,正如 False 只有消去规则一样,True 只有引入规则 True.intro : True。换句话说,True 就是真,并且有一个规范证明 True.intro

3.3.4. 逻辑等价(Logical Equivalence)🔗

表达式 Iff.intro h1 h2h1 : p qh2 : q p 产生 p q 的一个证明。表达式 Iff.mp hh : p q 产生 p q 的一个证明。类似地,Iff.mpr hh : p q 产生 q p 的一个证明。以下是 p q q p 的一个证明:

variable (p q : Prop) theorem and_swap : p q q p := Iff.intro (fun h : p q => show q p from And.intro (And.right h) (And.left h)) (fun h : q p => show p q from And.intro (And.right h) (And.left h)) and_swap p q : p q q p#check and_swap p q
and_swap p q : p  q  q  p
variable (h : p q) example : q p := Iff.mp (and_swap p q) h

我们可以使用匿名构造子符号从正向和反向的证明构建 p q 的一个证明,我们也可以在 mpmpr 上使用 . 符号。因此,前面的例子可以简洁地写成如下形式:

variable (p q : Prop) theorem and_swap : p q q p := fun h => h.right, h.left, fun h => h.right, h.left example (h : p q) : q p := (and_swap p q).mp h

3.4. 引入辅助子目标(Introducing Auxiliary Subgoals)🔗

这是一个介绍 Lean 提供的另一个有助于构建长证明的工具的好地方,即 have(拥有)构造,它在证明中引入一个辅助子目标。这里有一个小例子,改编自上一节:

variable (p q : Prop) example (h : p q) : q p := have hp : p := h.left have hq : q := h.right show q p from And.intro hq hp

在内部,表达式 have h : p := s; t 产生项 (fun (h : p) => t) s。换句话说,sp 的一个证明,t 是在假设 h : p 下对期望结论的一个证明,两者通过 λ 抽象和应用组合在一起。这个简单的工具在构建长证明时非常有用,因为我们可以使用中间的 have 作为通向最终目标的垫脚石。

Lean 还支持一种结构化的从目标向后推理的方式,它模拟了通常数学中的"只需证明(suffices to show)"构造。下一个例子只是将前面证明中的最后两行交换了位置。

variable (p q : Prop) example (h : p q) : q p := have hp : p := h.left suffices hq : q from And.intro hq hp show q from And.right h

suffices hq : q 给我们留下两个目标。首先,我们必须通过证明在额外假设 hq : q 下原始目标 q p 来说明证明 q 确实足够了。最后,我们必须证明 q

3.5. 经典逻辑(Classical Logic)🔗

我们目前所见到的引入和消去规则都是构造性的,也就是说,它们反映了基于命题即类型(propositions-as-types)对应的逻辑连接词的计算理解。普通的经典逻辑在此基础上增加了排中律(law of the excluded middle),p ¬p。要使用这个原理,你必须打开 classical 命名空间。

open Classical variable (p : Prop) em p : p ¬p#check em p
em p : p  ¬p

直观上,构造性的"Or"非常强:断言 p q 相当于知道哪一个成立。如果 RH 代表黎曼猜想,经典数学家愿意断言 RH ¬RH,即使我们还不能断言其中任何一个选言支。

排中律的一个结果是双重否定消去原理:

open Classical theorem dne {p : Prop} (h : ¬¬p) : p := Or.elim (em p) (fun hp : p => hp) (fun hnp : ¬p => absurd hnp h)

双重否定消去允许我们通过假设 ¬p 并推导出 False 来证明任意命题 p,因为这样就相当于证明了 ¬¬p。换句话说,双重否定消允许我们进行反证法(proof by contradiction),这在构造逻辑中通常是不允许的。作为练习,你可以尝试证明其逆命题,即证明 em 可以从 dne 推出。

经典公理还为你提供了额外的证明模式,可以通过诉诸 em 来证明。例如,你可以进行情况分析证明:

open Classical variable (p : Prop) example (h : ¬¬p) : p := byCases (fun h1 : p => h1) (fun h1 : ¬p => absurd h1 h)

或者你可以进行反证法证明:

open Classical variable (p : Prop) example (h : ¬¬p) : p := byContradiction (fun h1 : ¬p => show False from h h1)

如果你不习惯构造性思维,可能需要一些时间来感受经典推理在何处被使用。在下面的例子中需要它,因为从构造性的角度来看,知道 pq 并非同时为真,并不一定能告诉你哪一个为假:

example (h : ¬(p q)) : ¬p ¬q := Or.elim (em p) (fun hp : p => Or.inr (show ¬q from fun hq : q => h hp, hq)) (fun hp : ¬p => Or.inl hp)

我们稍后将看到,在构造逻辑中确实存在一些情况下,排中律和双重否定消去等原理是允许的,而 Lean 在此类上下文中支持使用经典推理而不依赖于排中律。

用于支持经典推理的完整公理列表在公理与计算(Axioms and Computation)中讨论。

3.6. 命题有效性示例(Examples of Propositional Validities)🔗

Lean 的标准库包含了许多命题逻辑有效语句的证明,你都可以在自己的证明中自由使用它们。以下列表包含一些常见的恒等式。

交换律:

  1. p q q p

  2. p q q p

结合律:

  1. (p q) r p (q r)

  2. (p q) r p (q r)

分配律:

  1. p (q r) (p q) (p r)

  2. p (q r) (p q) (p r)

其他性质:

  1. (p (q r)) (p q r)

  2. ((p q) r) (p r) (q r)

  3. ¬(p q) ¬p ¬q

  4. ¬p ¬q ¬(p q)

  5. ¬(p ¬p)

  6. p ¬q ¬(p q)

  7. ¬p (p q)

  8. (¬p q) (p q)

  9. p False p

  10. p False False

  11. ¬(p ¬p)

  12. (p q) (¬q ¬p)

这些需要经典推理:

  1. (p r s) ((p r) (p s))

  2. ¬(p q) ¬p ¬q

  3. ¬(p q) p ¬q

  4. (p q) (¬p q)

  5. (¬q ¬p) (p q)

  6. p ¬p

  7. (((p q) p) p)

标识符 sorry 神奇地产生任何命题的证明,或提供任何数据类型的对象。当然,作为一种证明方法它是不健全的——例如,你可以用它来证明 False——并且当文件使用或导入依赖于它的定理时,Lean 会发出严重警告。但它在逐步构建长证明时非常有用。从顶部开始向下编写证明,使用 sorry 来填充子证明。确保 Lean 接受了带有所有 sorry 的项;如果没有,说明存在需要纠正的错误。然后回过头来,将每个 sorry 替换为实际的证明,直到没有剩余。

这是另一个有用的技巧。你可以使用下划线 _ 作为占位符,而不是使用 sorry。回想一下,这告诉 Lean 该参数是隐式的,应该自动填充。如果 Lean 尝试这样做但失败了,它会返回一条错误消息"“不知道如何合成占位符”,后跟它期望的项的类型以及上下文中所有可用的对象和假设。换句话说,对于每个未解析的占位符,Lean 会报告当时需要填充的子目标。然后你可以通过逐步填充这些占位符来构建证明。

作为参考,以下是两个取自上面列表的有效性示例证明。

open Classical -- distributivity example (p q r : Prop) : p (q r) (p q) (p r) := Iff.intro (fun h : p (q r) => have hp : p := h.left Or.elim (h.right) (fun hq : q => show (p q) (p r) from Or.inl hp, hq) (fun hr : r => show (p q) (p r) from Or.inr hp, hr)) (fun h : (p q) (p r) => Or.elim h (fun hpq : p q => have hp : p := hpq.left have hq : q := hpq.right show p (q r) from hp, Or.inl hq) (fun hpr : p r => have hp : p := hpr.left have hr : r := hpr.right show p (q r) from hp, Or.inr hr)) -- an example that requires classical reasoning example (p q : Prop) : ¬(p ¬q) (p q) := fun h : ¬(p ¬q) => fun hp : p => show q from Or.elim (em q) (fun hq : q => hq) (fun hnq : ¬q => absurd (And.intro hp hnq) h)

3.7. 练习(Exercises)

证明以下恒等式,将 sorry 占位符替换为实际证明。

variable (p q r : Prop) -- commutativity of ∧ and ∨ declaration uses 'sorry'example : p q q p := sorry declaration uses 'sorry'example : p q q p := sorry -- associativity of ∧ and ∨ declaration uses 'sorry'example : (p q) r p (q r) := sorry declaration uses 'sorry'example : (p q) r p (q r) := sorry -- distributivity declaration uses 'sorry'example : p (q r) (p q) (p r) := sorry declaration uses 'sorry'example : p (q r) (p q) (p r) := sorry -- other properties declaration uses 'sorry'example : (p (q r)) (p q r) := sorry declaration uses 'sorry'example : ((p q) r) (p r) (q r) := sorry declaration uses 'sorry'example : ¬(p q) ¬p ¬q := sorry declaration uses 'sorry'example : ¬p ¬q ¬(p q) := sorry declaration uses 'sorry'example : ¬(p ¬p) := sorry declaration uses 'sorry'example : p ¬q ¬(p q) := sorry declaration uses 'sorry'example : ¬p (p q) := sorry declaration uses 'sorry'example : (¬p q) (p q) := sorry declaration uses 'sorry'example : p False p := sorry declaration uses 'sorry'example : p False False := sorry declaration uses 'sorry'example : (p q) (¬q ¬p) := sorry

证明以下恒等式,将 sorry 占位符替换为实际证明。这些需要经典推理。

open Classical variable (p q r : Prop) declaration uses 'sorry'example : (p q r) ((p q) (p r)) := sorry declaration uses 'sorry'example : ¬(p q) ¬p ¬q := sorry declaration uses 'sorry'example : ¬(p q) p ¬q := sorry declaration uses 'sorry'example : (p q) (¬p q) := sorry declaration uses 'sorry'example : (¬q ¬p) (p q) := sorry declaration uses 'sorry'example : p ¬p := sorry declaration uses 'sorry'example : (((p q) p) p) := sorry

证明 ¬(p ¬p) 而不使用经典逻辑。