overreactedby Dan Abramov

数学被鬼附身了

July 30, 2025

在过去的几个月里,我写了很多 Lean 代码。

Lean 是一门编程语言,但它主要被数学家使用。这很不寻常!这是因为 Lean 旨在将数学形式化。

Lean 让数学家可以像写代码一样对待数学——把数学拆分为结构、定理和证明,相互导入彼此的定理,并上传到 GitHub。

核心思想是,也许有一天,人类的大部分数学知识都可以以代码的形式存在——静态检查、可验证、可组合。

那么,使用 Lean 是什么感觉呢?


抱歉但不抱歉

为了让你感受一下 Lean,这里有一个极小的定理,声明 2 等于 2

theorem two_eq_two : 2 = 2 := by
  sorry

这里发生了什么?

在数学家的眼中,这种语法看起来像是在陈述一个定理。我们有 theorem 关键字、定理的名字、一个冒号 : 后面是定理的内容、我们想要证明的陈述,以及 := by 后面跟着证明(sorry 表示我们还没有完成实际的证明,计划以后补上)。

但如果你是程序员,可能会注意到别的东西。那个 theorem 看起来很像函数。但 2 = 2 又是什么?它看起来像是函数的返回类型。但 2 = 2 怎么会是类型呢?难道 2 = 2 不是布尔值吗?如果 2 = 2 真的是类型,那这个 2 = 2 类型的又是什么?这些都是很有趣的问题,但我们暂且先不深究。

我们先来看看这个证明:

theorem two_eq_two : 2 = 2 := by
  sorry

(你可以在 在线编辑器 里试试,虽然比不上 本地配置,但也能体验下。)

把光标放在 sorry 前面,右侧会有一个叫 Tactic state 的面板。此时,tactic state 显示 ⊢ 2 = 2

Goal is 2 = 2

这里, 表示目标,也就是你要证明的陈述。你当前的目标就是证明 2 = 2,所以 tactic state 显示 ⊢ 2 = 2

现在把光标放到 sorry 后面,会发现目标消失了:

No goals

目标没了!换句话说,你通过写 sorry“证明”了 2 = 2

当然,这毫无意义。你可以把 sorry 看作万能证明——它可以关闭任何目标。这是个谎言。从这个意义上说,sorry 就像 TypeScript 里的 any。它让你绕过证明检查器,但你实际上什么都没证明。

我们来试着去掉 sorry

现在你会看到证明不完整,目标也没有被解决。要真正证明 2 = 2,在下一行输入 rfl,就能成功关闭目标:

这里,rfl 意为“反身性”(reflexivity),来自“反射”,就像镜像一样。只要你遇到“镜像”目标,比如 something = somethingrfl 就能关闭它。你可以把 rfl 理解为内置的知识:“一个东西等于它自己”。

目标关闭后,你的证明就完成了。

theorem two_eq_two : 2 = 2 := by
  rfl

现在你已经证明了 two_eq_two,可以在其他地方引用这个事实。

theorem two_eq_two : 2 = 2 := by
  rfl
 
theorem two_eq_two_again : 2 = 2 := by
  exact two_eq_two

啊,模块化!

这里,two_eq_two_again 把证明的工作交给了 two_eq_two,因为当前目标(⊢ 2 = 2正好就是 two_eq_two 已经证明的内容。(对于程序员来说,这看起来就像返回了一个函数调用的结果。)

虽然这个例子很简单,但 exact some_other_theorem 在拆分复杂证明时非常有用,可以把大证明分成小定理逐步完成。

我们用到的这些命令——exactsorryrfl——被称为战术(tactics)。Lean 的证明(by 后面)就是一系列战术的组合。战术让你关闭不同的目标——rfl 用于 x = xexact some_other_theorem 用于你已经证明过的目标,sorry 可以关闭任何目标(但风险自负)。

要证明一个定理,你需要用恰当的战术直到所有目标都被关闭。


数学被鬼附身了

到目前为止,你已经证明了 2 = 2,这并不有趣。

我们来试试能不能证明 2 = 3

theorem two_eq_two : 2 = 2 := by
  rfl
 
theorem two_eq_three : 2 = 3 := by
  sorry

和之前一样,sorry 可以关闭任何目标,哪怕是 2 = 3

但这显然是作弊,我们还是想办法去掉 sorry

sorry 换成 rfl

这下可没那么容易了!你可以用 rfl 关闭 ⊢ 2 = 2,因为 2 = 2 形如 something = something。但目标 ⊢ 2 = 3 不是这种形式,所以 rfl 无法关闭 ⊢ 2 = 3

其实,这正是好事。 在大多数有用的数学理论中,2 = 3 是假命题,我们不希望 Lean 能证明假命题。

但其实,数学并不是一成不变的。数学是你自己定义的。如果你愿意,你可以让数学“闹鬼”,让 2 = 3 成立。

我们来引入一个公理,声明这一点:

axiom math_is_haunted : 2 = 3

axiom 就像 theorem,但它是凭信仰接受的。你可以把它看作 theorem math_is_haunted : 2 = 3 := by sorry,但更理直气壮。

现在你可以在其他证明中把这个 axiom 当作事实使用:

theorem two_eq_two : 2 = 2 := by
  rfl
 
axiom math_is_haunted : 2 = 3
 
theorem two_eq_three : 2 = 3 := by
  exact math_is_haunted

注意,这并不会报错!

这里,two_eq_three 的目标正好和 math_is_haunted 公理的陈述一致,所以我们用 exact 战术来关闭目标。

有了 math_is_haunted 和一些战术,你甚至可以证明更“邪门”的东西。比如,我们来证明 2 + 2 其实等于 6

theorem two_eq_two : 2 = 2 := by
  rfl
 
axiom math_is_haunted : 2 = 3
 
theorem two_add_two_eq_six : 2 + 2 = 6 := by
  -- 我们将在这里写点什么(顺便说一句,这是一条注释)

我们一开始的目标是 ⊢ 2 + 2 = 6

我们没有能直接解决这个目标的战术。但我们 math_is_haunted,它“证明”了 2 = 3。如果 2 真等于 3,那么要证明 2 + 2 = 6只需要证明 3 + 3 = 6

rewrite 战术可以帮我们做到这一点——把目标里的每个 2 都替换成 3

我们还是有一个未解决的目标,不过现在变成了 ⊢ 3 + 3 = 6

rewrite 战术就像在目标里“查找并替换”。如果你有一个 a = b 的证明,把它交给 rewrite,目标里的所有 a 都会变成 b。由于 math_is_haunted “证明”了 2 = 3rewrite [math_is_haunted] 就把目标从 ⊢ 2 + 2 = 6 变成了 ⊢ 3 + 3 = 6

现在我们的目标是 ⊢ 3 + 3 = 6,这就容易多了。实际上,rfl 战术就能关闭这个目标,从而完成整个证明:

theorem two_eq_two : 2 = 2 := by
  rfl
 
axiom math_is_haunted : 2 = 3
 
theorem two_add_two_eq_six : 2 + 2 = 6 := by
  rewrite [math_is_haunted]
  rfl

(这里,rfl 能关闭 ⊢ 3 + 3 = 6,但原因可能和你想的不一样。它其实并不“知道” 3 + 36。实际上,rfl 会把两边的定义都展开再比较。当 36+ 被展开后,两边都会变成类似 Nat.zero.succ.succ.succ.succ.succ.succ 的形式。所以这其实还是something = something 的情况,rfl 就能关闭它。)

这样一来,我们就“成功”证明了 2 + 2 = 6

这是不是有点毛骨悚然?事实上,math_is_haunted 这个公理太邪门了,它甚至能推导出矛盾(比如 2 + 2 = 62 + 2 ≠ 6 可以同时被证明),按照逻辑的规律,这意味着我们现在什么都能证明了。

在这里,是我们自己故意加了 math_is_haunted,所以这也怪不得别人。但实际上,上世纪初还真发生过类似的事。人们发现,构建大部分数学的集合论中有一个矛盾源自某个公理。后来通过选择不同的公理“修补”了这个问题,但这也让数学界陷入了焦虑、脱发和灵魂拷问。

现在我们把 axiom math_is_haunted 删掉。这样一来,依赖这个邪门公理的 two_add_two_eq_six 证明就坏了:

这正是我们想要的!坏掉的东西就不应该能通过证明检查。

要修复它,我们把命题改成 2 + 2 = 4,这才是正确的(根据 Lean 所熟悉的自然数公理):

没有了那个坏公理,数学就不再“闹鬼”了!(或者说,我们只能希望如此

用“胡说八道的数学”来介绍 Lean 可能有点奇怪,毕竟大多数人在 Lean 里做的数学其实都很严肃。但我认为,这很有力地说明了和证明检查器打交道的本质。

证明检查器只验证你选定公理下逻辑推理的有效性。它让你用 rewriterflexact 以及更多战术,串联起各种逻辑变换,逐步证明越来越复杂的数学结构和定理。

只要你的公理是可靠的,Lean 本身也是可靠的,你的结论就可靠。无论你的证明只是一个 rfl,还是几百万行 Lean 代码,这都成立。


费马大定理

举个极端的例子,来看一下费马大定理。它说,对于任意大于 2 的 n,不存在三个正整数 xyz 满足 xⁿ + yⁿ = zⁿ

import Mathlib
 
theorem PNat.pow_add_pow_ne_pow (x y z : ℕ+) (n : ℕ) (hn : n > 2) :
    x^n + y^n ≠ z^n := by
  sorry

这个定理在被提出后 350 多年,直到 1994 年才被证明,证明过程长达 100 多页。

现在有一个正在进行的项目,试图用 Lean 形式化这个定理的证明,预计将耗时数年。虽然命题本身很简单,但证明需要建立许多数学结构和定理

如果你克隆 FLT 仓库并打开 FermatsLastTheorem.lean,你会看到一个证明,但它其实依赖了 sorry,可以通过打印它的公理看出来:

#print axioms PNat.pow_add_pow_ne_pow
/-
'PNat.pow_add_pow_ne_pow' depends on axioms: [propext, sorryAx, Classical.choice, Quot.sound]
-/

但等到所有子证明都被形式化,这个项目完成时,pow_add_pow_ne_pow 所依赖的证明就不会再有 sorry#print axioms PNat.pow_add_pow_ne_pow 里也不会再出现 sorryAx

我敢打赌,合并那个最终消灭所有 sorry 的 PR 时一定会非常爽!


下一步

显然,今天我们并没有证明什么有用的东西。看起来为了搞清楚 2 + 2 = 4 这样的小事要做很多工作。但,嗯……你应该也感受到了一点特别的东西。它有点像编程,又有点不一样。如果你对 Lean 感兴趣,我推荐几个资源:

  • 先试试 自然数游戏,这是一个非常温和又有趣的 Lean 入门教程。顺便还能学到自然数到底是什么。
  • Mathematics in Lean 的前几章很容易上手,也不要求数学背景。我觉得它们很适合熟悉基本战术。
  • 我最喜欢的数学书 Tao 的《分析》,现在有了 Lean 伴侣,而且在 GitHub 上持续更新
  • Lean Zulip 社区的“new members”频道非常友好。

虽然我没打算写 Lean 入门教程(自然数游戏和 Mathematics in Lean 已经足够好了),但我大概率会写一些具体的“啊哈”时刻,比如我前面提到的“2 = 2 其实是一个类型”之类的话题。Lean 融合了数学和编程领域丰富历史中的许多令人脑洞大开的思想,我在重新发现这些内容时感到非常快乐。我希望有更多人能无目的地试试 Lean——它真的很有趣

当然,这要看你是不是那种喜欢的人了。

Pay what you like

Discuss on Bluesky  ·  Edit on GitHub