LEAN - 2. 命题和证明

到目前为止,您已经看到了一些在 Lean 中定义对象和函数的方法。在该章节,我们将开始解释如何用依赖类型理论的语言编写数学断言和证明。

作为类型(Type)的命题

证明有关以依赖类型理论语言定义的对象的断言的一种策略是在定义语言之上分层断言语言assertion language)和证明语言proof language)。但是没有必要以这种方式增加新语言,依赖类型理论的灵活让我们可以在同一个通用框架中表示断言和证明。

例如,我们可以引入一种新类型Prop来表示命题,并引入构造函数来从其他类型中构建新命题。

def Implies (p q : Prop) : Prop := p → q
#check And     -- Prop → Prop → Prop
#check Or      -- Prop → Prop → Prop
#check Not     -- Prop → Prop
#check Implies -- Prop → Prop → Prop

variable (p q r : Prop)
#check And p q                      -- Prop
#check Or (And p q) r               -- Prop
#check Implies (And p q) (And q p)  -- Prop

然后,我们可以引入p : Prop表示命题,同时引入另一种类型的Proof p,用于表示p的证明。“公理”将是这种类型的常数。

def Implies (p q : Prop) : Prop := p  q
structure Proof (p : Prop) : Type where
  proof : p
#check Proof   -- Proof : Prop → Type

axiom and_comm (p q : Prop) : Proof (Implies (And p q) (And q p))

variable (p q : Prop)
#check and_comm p q     -- Proof (Implies (And p q) (And q p))

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

从证明Implies p q和证明p,我们能获得q的证明.

我们可以这样表示:

def Implies (p q : Prop) : Prop := p  q
structure Proof (p : Prop) : Type where
  proof : p
axiom modus_ponens : (p q : Prop)  Proof (Implies p q)   Proof p  Proof q

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

p为以一个假设(hypothesis),我们有q的证明。然后我们能 cancel 这个假设得到Implies p q的证明.

我们可以这样表示:

def Implies (p q : Prop) : Prop := p  q
structure Proof (p : Prop) : Type where
  proof : p
axiom implies_intro : (p q : Prop)  (Proof p  Proof q)  Proof (Implies p q)

这种方法将为我们提供建立断言和证明的合理方法。确定表达式t是断言p的正确证明,只需检查t是否具有类型证明p

我们可以做一些简化。首先,我们可以通过将Proof pp本身混为一谈来避免重复编写证明一词。换句话说,只要我们有p : Prop,我们就可以将p解释为一个类型,即它的证明的类型。 然后我们可以将t : p解读为tp的证明。

此外,一旦我们进行了这种识别,蕴含规则表明我们可以在Implies p qp → q之间来回传递。 换句话说,命题pq 间的蕴涵对应于具有将p的任何元素带入 q 的元素的函数。 因此,连接Implies的引入是完全多余的:我们可以使用依赖类型理论中通常的构造函数p → q作为蕴涵记号。

这是构造演算中遵循的方法,在 Lean 中也是如此。自然演绎证明系统中的蕴涵规则与管理函数的抽象和应用的规则完全一致,这一事实是 Curry-Howard 同构的一个实例,有时称为命题即类型范式。事实上,类型PropSort 0的语法糖,是上一章中描述的类型层次结构的最底层。此外,类型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为真这一事实之外,它们不携带任何信息。

我们提出的关于命题作为类型范式的两种思考方式在根本上是不同的。从建设性的角度来看,证明是抽象的数学对象,由依赖类型理论中的适当表达式表示。相反,如果我们按照上面描述的编码技巧来思考,那么表达式本身并不表示任何有趣的东西。相反,我们可以把它们写下来并检查它们的类型是否正确,以确保所讨论的命题是正确的。换句话说,表达式本身就是证明。

在下面的说明中,我们将在这两种说话方式之间来回切换,有时说一个表达式“构造”(constructs)或“产生”(produces)或“返回”(returns)一个命题的证明,有时只是说它“是”(is)这样的证明。 这类似于计算机科学家有时模糊句法和语义之间的区别的方式,有时说程序“计算”某个函数,有时说程序“是”所讨论的函数。

无论如何,真正重要的是底线。为了用依赖类型理论的语言正式表达一个数学断言,我们需要展示一个术语p : Prop。为了证明这个断言,我们需要展示一个术语t : p。作为证明助手,Lean 的任务是帮助我们构造这样的项t,并验证它的格式是否正确且类型正确。

使用作为类型的命题

在作为类型的命题范式中,仅涉及的定理可以使用lambda 抽象和应用来证明。在 Lean 中,定理命令引入了一个新定理:

variable {p : Prop}
variable {q : Prop}

theorem t1 : p → q → p := fun hp : p => fun hq : q => hp

这看起来与上一章中常量函数的定义一模一样,唯一的区别是参数是Prop的元素而不是Type。直观地说,我们对p → q → p的证明需要事先假设pq为真,并使用第一个假设来确定结论p为真。

请注意,theorem命令实际上是def命令的一个版本:在命题和类型对应下,证明定理p → q → p实际上与定义关联类型的元素相同。对于内核类型检查器来说,两者没有本质区别。

然而,定义和定理之间存在一些实际差异。在正常情况下,没有必要展开定理的“定义”;通过证明无关性,该定理的任何两个证明在定义上都是相等的。 一旦定理的证明完成,通常我们只需要知道证明存在即可;证明是什么并不重要。鉴于这一事实,Lean 将证明标记为不可约,这向解析器(更准确地说,阐述器)提示,在处理文件时通常不需要展开它。事实上,Lean 通常能够并行处理和检查证明,因为评估一个证明的正确性不需要知道另一个证明的细节。

与定义一样,#print命令将向您展示定理的证明。

variable {p : Prop}
variable {q : Prop}
theorem t1 : p → q → p := fun hp : p => fun hq : q => hp

#print t1

请注意,lambda 抽象hp : phq : q可以被视为t1证明中的临时假设。 Lean 还允许我们使用show语句明确地指定最终术语hp的类型。

variable {p : Prop}
variable {q : Prop}
theorem t1 : p → q → p :=
  fun hp : p =>
  fun hq : q =>
  show p from hp

添加此类额外信息可以提高证明的清晰度,并有助于在编写证明时检测错误。show命令只是对类型进行注释,并且在内部,我们看到的所有t1表示都会产生相同的术语。

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

variable {p : Prop}
variable {q : Prop}
theorem t1 (hp : p) (hq : q) : p := hp

#print t1    -- p → q → p

现在我们可以将定理t1应用为函数应用程序。

variable {p : Prop}
variable {q : Prop}
theorem t1 (hp : p) (hq : q) : p := hp

axiom hp : p

theorem t2 : q → p := t1 hp

在这里,公理声明假定存在给定类型的元素,并且可能会损害逻辑一致性。 例如,我们可以用它来假设空类型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

#print t1

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

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

如果 pq 已被声明为变量,Lean 会自动为我们概括它们:

variable {p q : Prop}

theorem t1 : p → q → p := fun (hp : p) (hq : q) => hp

事实上,通过将命题视为类型,我们可以将p成立的假设hp声明为另一个变量:

variable {p q : Prop}
variable (hp : p)

theorem t1 : q → p := fun (hq : q) => hp

Lean 检测到证明使用hp并自动添加hp : p作为前提。 在所有情况下,#print t1命令仍然产生 ∀ p q : Prop, p → q → p。 请记住,这种类型也可以写成∀ (pq : Prop) (hp : p) (hq :q), p,因为箭头仅表示目标不依赖于绑定变量的箭头类型。

当我们以这种方式推广t1时,我们可以将其应用于不同的命题对,以获得一般定理的不同实例。

theorem t1 (p q : Prop) (hp : p) (hq : q) : p := hp

variable (p q r s : Prop)

#check t1 p q                -- p → q → p
#check t1 r s                -- r → s → r
#check t1 (r → s) (s → r)    -- (r → s) → (s → r) → r → s

variable (h : r → s)
#check t1 (r → s) (s → r) h  -- (s → r) → r → s

再一次,使用命题作为类型的对应,类型r → s的变量h可以被视为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₁ (hh₃)

作为命题逻辑的定理,t2说了什么?

请注意,使用数字unicode下标(输入为\0\1\2、...)通常很有用,就像我们在本例中所做的那样。

命题逻辑

精益定义了所有标准的逻辑连接词和符号。 命题连接词带有以下符号:

Ascii Unicode Editor shortcut Definition
True True
False False
Not ¬ \not, \neg Not
/\ \and And
/ \or Or
-> \to, \r, \imp
<-> \iff, \lr Iff

它们都在Prop中取值。

variable (p q : Prop)

#check pqpq
#check ¬ppFalse
#check pqqp

操作顺序如下:一元否定¬结合最强,然后是,然后是,然后是,最后是。例如,a ∧ b → c ∨ d ∧ e表示 (a ∧ b) → (c ∨ (d ∧ e))。 请记住, 关联到右侧(现在没有任何变化,因为参数是Prop的元素,而不是其他类型),其他二元连接词也是如此。 所以如果我们有p q r : Prop,表达式p → q → r 读作“如果 p,那么如果 q,那么 r”。 这只是 p ∧ q → r 的“咖喱”形式。

在上一章中,我们观察到lambda 抽象可以被视为符号的一种“引入规则”。在当前设置中,它显示了如何引入或建立暗示(implication)。lambda 应用可以被视为“消除规则”,显示如何消除或在证明中使用暗示。其他命题连接词在 Lean 库的 Prelude.core 文件中定义(有关库层次结构的更多信息,请参阅导入文件),每个连接词都有其规范的引入和消除规则。

连词

表达式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

#check fun (hp : p) (hq : q) => And.intro hp hq

示例说明了一个定理,但没有命名它或将其存储在永久上下文中。 本质上,它只是检查给定术语是否具有指定的类型。它便于说明,我们会经常使用它。

表达式And.left h从证明h中创建p的证明:p ∧ q。 类似地,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)

posted on   Black_x  阅读(517)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示