Lean 4 证明分析
Created on: January 17, 2025
Created on: January 17, 2025
我正在使用Lean4,但是我不是很懂这个语言,如下有一段代码,请帮我分析讲解一下
import Mathlib
import Aesop
set_option maxHeartbeats 0
open BigOperators Real Nat Topology Rat
theorem solve_z_solution : 2 * z + 1 = z → z = 1
| h => by
{
have h : 2 * z + 1 = z := h
omega
}
下面这段 Lean 4 代码的逻辑非常简单,做的事情是证明“若 ,则 ”。代码如下:
leanimport Mathlib import Aesop set_option maxHeartbeats 0 open BigOperators Real Nat Topology Rat theorem solve_z_solution : 2 * z + 1 = z → z = 1 | h => by { have h : 2 * z + 1 = z := h omega }
以下分几部分为你讲解:
import Mathlib
:导入 Lean 4 Mathlib 库,这是社区维护的数学库,其中提供了许多基本的数学定义和定理。import Aesop
:导入 Aesop,是 Lean 4 的一个自动化证明工具(tactic)库,能帮助简化或自动完成一些推理。set_option maxHeartbeats 0
:将“心跳数(heartbeats)”限制取消,避免在复杂证明或自动化证明时超时。在 Lean 中,“heartbeats”是一个用来限制搜索深度的机制。open BigOperators Real Nat Topology Rat
:把若干命名空间(BigOperators
、Real
、Nat
、Topology
、Rat
)下的定义、定理等放到当前命名空间,书写上更方便。leantheorem solve_z_solution : 2 * z + 1 = z → z = 1
theorem solve_z_solution : 2 * z + 1 = z → z = 1
可以看作一个函数类型:给定前提 2 * z + 1 = z
,输出结论 z = 1
。lean| h => by { have h : 2 * z + 1 = z := h omega }
| h => by ...
是 Lean 4 的一种“匹配参数”写法,表明如果我们的函数/定理接收到一个形参(此处即前提 2 * z + 1 = z
),将其命名为 h
,然后进入 by
后面的 tactic 块,进行证明。
have h : 2 * z + 1 = z := h
h
”(其实和外部的 h
同名),内容与原来的 h
一致:
h
,无需再用 have h
重命名。omega
这是一个自动解决“线性算术方程或不等式”类问题的 tactic。它擅长在形如“”、“”、“” 等线性表达式中一键求解。
在这个例子里,omega
可以从 2 * z + 1 = z
推出 z = 1
,因为它会自动进行类似:
(注意:这里要看你的变量类型、上下文等,一般在自然数或整数环境中才有意义。如果 z
允许是整数,那么正确答案是 z = -1
;若 z
是实数,则推出 z = -1
。不过你的定理里结果是 z = 1
,所以可能在具体上下文中你对 有另外的假设,或是想证明某个固定值。不过就代码本身而言,它自动会尝试化简并得到结论。)
若是在 Lean 3 或 Lean 4 里的 mathlib 环境下,一般 omega
或者 linarith
都是解决此类线性算术的利器。
需要特别留意的是,单纯从方程 直接化简,会得出 。然而你的定理写的是
z = 1
。如果这是一个演示性质的例子,那么要么是想演示如何用omega
解决“线性方程证明”的框架,要么是上游条件有别的附加假设(如某个地方写错了演示而已)。
如果这段代码能在 mathlib 环境下正常通过,那就说明 Lean 的上下文中,也许z
并非一般的自由变量,或者在别的地方有前置限制(例如:z : ℕ
并且 Lean 中做了其他假设使得推理结果变成z = 1
,等等)。实际情况要结合你项目中的上下文或额外假设来判断。
omega
tactic 可以自动处理线性算术证明,将人力从繁琐的运算中解放出来。z = 1
,则需要确认你的上下文(如对 z
的额外约束)是否与此兼容,或者这只是一个演示例子。希望这能帮助你理解 Lean 4 中这段代码的写法和使用思路。
首先明显"2 * z + 1 = z → z = 1"这个定理是错的,我以为lean4在证明错误定理时,会给出一个类似于"报错"的信息,但其实没有,lean运行完就结束了。lean不是用来做定理证明的吗?为什么在面对错误定理的时候不报错呢
在初学 Lean(或其他同类的定理证明器)时,很多人会产生一个直观的疑惑:
「明明这个定理在数学上是“错误”的(例如从 推出 ),为什么 Lean 接受了这个“证明”,也不报错呢?」
其实,这背后涉及到逻辑语言和自动证明的几个关键点:
Lean 的内核(kernel)在做的事情是:检查你给出的**“证明脚本(proof term)”是否与声明的“结论”匹配,并确保整个过程在 Lean 的逻辑规则体系内自洽**。如果证明脚本(即一系列 tactic 拼接而成)成功构造出一个合法的证明对象,Lean 就会说“好,这个定理通过了”。
对于「形如 P → Q
」的命题,只要 Lean 能通过一系列推理(或 tactic)验证“在假设 P
的条件下可以得出结论 Q
”,那它就会接受此定理,不会报错。
P → Q
恒为真」我们常在初等逻辑里学过:
如果 本身是“永假”或者“无解”、永远不可能满足,那么 在逻辑上是真的——这称为“真值表”或“vacuous truth(真空真)”:
只要「」在当前上下文里根本没有可行解,那么这个命题在逻辑上就成立了:因为它从一个“不会发生的前提”推出了任意结论,这在经典逻辑中是允许的(前提永假时,P -> Q
就为真)。
因此,如果 Lean 里并没有限制 为某个可以让 成立的类型或取值,那么 Lean 只会看到「你给了一个前提 2z+1=z
,但是这个前提实际上在这环境下是 false(或不可满足)。那就等价于“false → z=1”,逻辑上这自动为真。」内核不会认为这是什么矛盾,也就“欣然接受”。
进一步说,Lean(以及大部分定理证明系统)都遵循经典逻辑或某种构造逻辑的规则,其中有条著名原则:
ex falso quodlibet(从假可得任意结论)
也就是说,如果某一步推理中我们“假设”了一个矛盾式(如「 在特定上下文里无解」)或直接得到了“False”,那么在那之后,你想要证明“1=2”也好,“z=1”也好,理论上都能推出——因为“从假可得任意结论”。在 Lean 里,如果 tactic 能够看出前提不可满足,就能自动用这个逻辑原则,把你想要的结论强行“证明”出来。
因为在 Lean 看来,这条定理并不是“自相矛盾”或者“与其余上下文冲突”的东西:它是一条条件命题(→
),而前提恰好在当前环境下无法满足,于是它就被判定为真。Lean 的内核只要能 type-check 整个证明,就不会抛出报错。
一个类比是:
false -> anything
成立。2z+1=z
在你给定的类型/假设里确实可解、却解不出 z=1
。然后看看 Lean 是否真的能让leanvariable (z : ℤ) -- z 是任意整数 theorem my_thm (h : 2*z + 1 = z) : z = 1 := by ...
omega
或 linarith
完成这个证明。如果 Lean 仍然能完成,那说明 Lean 逻辑上判断这个前提对于整数是不可满足的(但其实对整数来说有解 z=-1 啊?)就会报错或无法完成,因为 Lean 会发现“2*z+1=z ⇒ z=-1”的推导,与 z=1
冲突。这种情况下,Lean 可能自动化战术失败或报错“目标无法证明”,从而无法完成定理。z=1
”显然冲突,就会失败。于是 Lean 就会告诉你,无法完成该定理,也就不会接受它作为“已证事实”。omega
)直接用“从假可得任意结论”的方式完成了证明。所以结论就是:
Lean 不会对 “” 报错,是因为在它的逻辑规则中,这可能是一个从“假设不可成立”到“任意结论”都可以的 vacuous truth,而非真正的矛盾。
要想让 Lean 确切地告诉你“这确实推不出来”,就要在环境中让该前提确实可成立、并且与结论冲突,这时 Lean 的自动化证明就会失败,从而“报错”或无法完成定理。
Lean中有什么办法判断这种前提是否有错吗?
我不想再遇到这种由于前提是错的,所以导致尽管命题有问题,我也发现不了的情况。
在 Lean(乃至绝大部分主流定理证明器)里,“前提是否可行/是否有解”这一点并不会被系统自动“保驾护航”——Lean 内核本身不做可满足性检测,它只负责验证“给定前提之下,你的证明脚本是否能构造出结论”。如果前提在当前环境下不可满足(即“假命题”),那么根据经典逻辑的“前提永假则 成立”原则,这样的定理就会被 Lean 接受。
然而,在实际数学或形式化工程中,我们常常想尽早发现“前提其实无解”或“与已知条件冲突”之类的问题。下面介绍几种在 Lean 中主动检查前提可满足性的思路与技巧,帮助你及时发现错误定理。
很多时候,引起“前提真空”的原因是:我们在 Lean 里写了一个“过于泛型化”的定理声明,比如没有指明 z
是哪种类型。Lean 会把 z
当成一个完全任意的类型中的元素,这可能导致 2*z + 1 = z
在“没有任何额外约束”的世界里是“永假”,从而 → z = 1
就 vacuously true 了。
做法:
variable
或在定理声明中加类型限定。例如让 z : ℤ
(整数)、z : ℕ
(自然数)等。这样 Lean 的自动化算术工具(如 linarith
, omega
)就能对该类型上的方程进行真正的化简和求解。示例:
leantheorem test_z (z : ℤ) (h : 2*z + 1 = z) : z = 1 := by omega -- or linarith
在整数类型下,2*z + 1 = z
化简后得到 z = -1
,跟“z = 1
”冲突。此时 Lean 的自动化工具会无法完成这个证明脚本,报出类似“tactic failed to prove the goal”,从而暴露出「前提可满足,但想推 z = 1
是假的」。
Lean 中常用的线性算术自动化 tactic 有:
linarith
:适用于线性方程组/不等式等。omega
:针对整数/自然数模型下的 Presburger 算术。如果你想要知道一个假设在 Lean 当前环境下是否矛盾,可以尝试在同一个 proof block 中使用 linarith
或 omega
,令其帮你看看能否推出不等式冲突或其他矛盾。
示例:
leanexample (z : ℤ) (h : 2*z + 1 = z) : False := by -- 如果 2z+1=z 在 ℤ 下可解且会导致其他冲突,可以试着让 linarith 检查 linarith
linarith
成功了,说明它推出了矛盾 (False)。linarith
失败了,它会报“no contradiction found”等,说明前提并不与当前所有假设冲突,或者不够线性可解。在 z : ℤ
的情况下,其实 2*z + 1 = z
有解 z = -1
,是完全一致的,不会直接推出 False
。不过如果你接着说“然后证明 z = 1
”,就和 z = -1
碰撞了,这才会让自动化工具报错。
有时我们并不是要用前提推结论,而是先想知道“这个前提到底有没有解?”。在 Lean 里可以通过**构造一个“辅助定理”**的方式来探路:
leanexample (z : ℤ) (h : 2*z + 1 = z) : z = -1 := by linarith
z = -1
。leanexample (z : ℕ) (h : 2*z + 1 = z) : False := by linarith
ℕ
里,2*z + 1 = z
无解,linarith
可以直接发现 2z + 1 >= 1
,与 z
的值不匹配时得到矛盾,从而成功证明 False
。通过这种做法,你就能显式地**“检验”**某个假设在给定类型下是否自相矛盾。
虽然 #check
常用于查看定义类型,#reduce
/#eval
常用于简化表达式,但有时候它们也能帮你做一些小规模的算术检验。比如说如果 z
是个具体数值、或你能在 Lean 里做某些归纳检验,就可以用它们来看看 Lean 对某段表达式的归约结果是否如你所料。
不过对于“2*z + 1 = z
在抽象的 ℤ
或 ℕ
下有没有解”这种问题,这些命令就不如 linarith
/omega
来得直观。
linarith
, omega
等)**尝试证明“该前提 ⇒ 矛盾”或“该前提 ⇒ 某具体解”。h : P ⇒ ∃ x, ...
,要么证明 h : P ⇒ False
。如果 Lean 说“行,能成功证明”,就说明 P
真的不可满足;如果 Lean 说“行,可以给出某具体解”,说明 P
可满足。通过这些手段,你就可以在 Lean 里主动发现像「 在当前上下文里是否真是“假命题”」的问题,从而避免因为 vacuous truth 而不小心让 Lean 接收了“错误定理”。