编译通过为什么就是定理得证:形式化方法系列导引
形式化方法课程开学第一周通常会出现一个让 CS 背景的学生愣住的演示:教师在 Lean 4、Coq 或 Agda 里敲完几行代码,按下保存键,编辑器右侧弹出 No goals 或者编译进程打印 Build completed successfully,然后教师宣布:定理已被证明。
让人愣住的不是命题本身,而是这条等式:编译通过 = 定理得证。命令式语言里编译通过只意味着语法和类型对得上,离"这段程序做的事是对的"还远;可在形式化系统里,编译通过却被当成数学意义上的"得证"。
这条等式要怎样才能成立?需要类型系统强到什么程度?类型检查器到底替人类干了哪些活、又留下哪些活没干?这个系列从这个具体问题展开,写给硕士一年级、刚开形式化方法课、有 CS 背景但还没真正用过证明助手的读者。
一个具体场景
Lean 4 里写一条命题以及它的证明可以是这样:
1 | |
把这段代码塞给 Lean 4 编译器,没有任何错误信息。从工程视角看,“编译通过"就是这件事的全部;从课程视角看,编译器刚刚验证了"对任意命题 P、Q,从 P ∧ Q 可以推出 Q ∧ P”。
紧挨着写一段处理对偶的函数:
1 | |
抹掉 theorem/def 的区别和类型层(Prop/Type),两段代码的主体几乎一致:都是 fun ... => ⟨..., ...⟩ 这种"拆开 / 反向打包"的小程序。这不是巧合。系列要花很长篇幅说清楚的事情,浓缩成一句就是:
证明就是程序,命题就是类型,证明检查就是类型检查。
这条命题叫 Curry-Howard 同构,是把"编译通过 = 定理得证"撑起来的核心结构。
三种直觉的汇合
读者打开形式化方法课程时,至少带着三种已有直觉。这三种直觉在课程开始前往往是分开的,课程的工作之一就是让它们合流。
| 来源 | 已有直觉 | 在系列里要做的事 |
|---|---|---|
| 编程 | 类型系统能拒绝"明显写错"的程序,但通常和"程序是不是符合需求"无关 | 升级为:类型系统可以表达"程序符合规约"这件事本身 |
| 逻辑 | 证明是一棵自然演绎树,节点是规则,叶子是公理或假设 | 升级为:那棵树本身可以重新读成一段良类型的程序 |
| 数学 | 证明是文字+符号写成的论证,需要同行评议 | 升级为:把同行评议的工作拆出"机械步骤",交给类型检查器去算 |
三种直觉合流后,"编译通过 = 定理得证"就不再是修辞,而是字面意义。
把"编译通过即定理得证"拆成三层翻译
要让等式成立,需要三层翻译同时对位:
| 逻辑端 | 类型端 | 程序端 | 检查端 |
|---|---|---|---|
命题 P |
类型 P |
— | — |
假设 H : P |
变量 H : P |
形参 H : P |
— |
P → Q 的证明 |
函数类型 P → Q 的元素 |
函数 fun h => ... |
子程序的类型 |
P ∧ Q 的证明 |
积类型 P × Q 的元素 |
二元组 ⟨a, b⟩ |
各分量的类型 |
P ∨ Q 的证明 |
和类型 P ⊕ Q 的元素 |
Sum.inl a / Sum.inr b |
标签 + 子项类型 |
∀ x : A, P x 的证明 |
依值函数类型 (x : A) → P x 的元素 |
依值函数 | 函数体在每个 x 下的类型 |
∃ x : A, P x 的证明 |
依值积类型 (x : A) × P x 的元素 |
依值二元组 | 见证 + 性质 |
| 证明检查 | 类型检查 | — | 编译器内核 |
最右边一栏是这套等式真正的引擎:编译器内核拿到一段程序,按一组固定的类型规则一步步检查,整段程序通过类型检查 ⟺ 中间那棵语法树是一棵合法的证明树。
后续文章会逐行展开这张表,本文只先点出整体形状。
系列要回答的主线问题
主线问题保持不变:
为什么形式化编译器编译通过,就证明命题得证?
围绕这个主线,系列要分别回答:
- 在构造主义视角下,“什么算一个证明"为什么能简化成"构造一个见证物”。
- 命题、类型、程序之间的对应到底有多严格,哪些逻辑联结词对应哪些类型构造子。
- 简单类型 lambda 演算 (STLC) 这种最小系统已经能表达哪些命题逻辑。
- 为什么类型检查算法在足够好的系统里就是证明检查算法,而且是机械、可终止的。
- 依值类型、归纳类型怎样把表达力从命题逻辑推到一阶逻辑,再推到日常数学。
- 真上手 Lean 4 之后,写定理、跑 tactic、读类型错误的实际体验是什么样的。
- 类型检查器的可信基底(TCB)由哪些东西组成;坏公理、
sorry、不一致定义会从哪里入侵。 - CompCert、seL4、Four Color Theorem、Feit-Thompson 这些大型形式化案例分别证了什么、代价是什么、可信度是什么。
- 形式化方法在工程上的局限和取舍,与传统测试、模型检查的关系。
每一个问题对应系列里 1 到 2 篇文章,不会跳着写。
系列地图
1 | |
工具选择
主线工具选 Lean 4,原因有三个:
- 文档完善、社区活跃,Mathlib 已经积累了大量样例,从代数到拓扑再到数值分析都有可读的真实证明。
- 既能当通用编程语言(产出可执行二进制),也能当证明助手,能自然演示"证明就是程序"的双重身份,这正是系列的主线。
- 元编程接近现代编程语言习惯,对 CS 背景读者门槛比 Coq 略低。
不强求读者立刻装本地环境。Lean 官方提供了 Web 编辑器,可以直接在浏览器里跑示例。系列第 07 篇会专门讲本地环境搭建。
辅助工具:
- 早期讨论 STLC、BHK 解释时用 Python 写一个最小类型检查器,跟读者已有的"自己写解释器"经验衔接。
- 引用 Coq 与 Agda 的语法做对比,但不要求读者搭这两套环境。
给读者的预期
读者画像:
- 修过离散数学、一阶逻辑、可计算性,能读 BNF、推导规则、归纳定义。
- 写过命令式语言(C / Java / Python),对函数式语言有零星接触。
- 对"编译器"有直觉(gcc / javac / rustc 用过),但没把编译器当过证明检查器。
- 没系统用过 Coq / Lean / Agda。
预期收获:
- 看完系列后能说清楚"编译通过 = 定理得证"在什么前提下成立、什么前提下不成立。
- 能在 Lean 4 里独立写出 20 行以内的证明,能读懂常见 tactic。
- 能给一段非平凡的工程程序判断"形式化是否值得",并给出可解释的理由。
不在系列覆盖范围内的话题:
- 同伦类型论、立方类型论的内部机制(点到为止,留给延伸路线)。
- 范畴论、Curry-Howard-Lambek 三角的范畴侧(同上)。
- SAT/SMT 求解器、模型检查、TLA+ 的细节(属于"形式化方法"这把大伞下的另一支,本系列不主动展开)。
写作约束
每篇文章的写作约束抄录自仓库内 writing-plans/形式化方法导引系列写作计划.md(这份计划不进 Hexo 发布流,只服务后续协作):
- 一篇只回答一个核心问题。
- 含代码必须实际运行,文章输出与真实输出一致。
- 至少一处把概念映射到读者熟悉的工程对象。
- 落盘前扫常见 AI 味与拟人味表达。
阅读次序与写作次序一致,不必跳读,但允许只读概念骨架(前 6 篇)或只读工程实践(后 6 篇)的人挑各自的子集。
一段提前的告诫
"编译通过 = 定理得证"读起来像免费午餐,工程实践里却不是。系列在收束时会把账算清楚:
- 编译通过依赖一个最小可信内核(kernel),内核之外的一切都是元数据。
- 内核之外不假思索引入的 axiom 可能让整套系统变成"任何命题可证",得证不再有意义。
- 形式化把"哪部分被信"明确化,但并不让人脱离信任:信变了形状,没有消失。
- 大型形式化案例(CompCert、seL4、Mathlib)展示的是"在多少代价之下能换到多少可信度",不是"形式化让 bug 消失"。
后续文章会一项一项把这些代价摊开,让读者在课程结束时既能写证明,也能判断什么时候不该写证明。
下一篇从 BHK 解释入手,讨论"什么算一个证明"在构造主义视角下被怎样重新定义,以及这次重新定义为什么是 Curry-Howard 同构能成立的前提。
参考资料
- Philip Wadler. Propositions as Types. Communications of the ACM, 2015. https://dl.acm.org/doi/10.1145/2699407
- Jeremy Avigad, Leonardo de Moura, Soonho Kong, Sebastian Ullrich. Theorem Proving in Lean 4. https://leanprover.github.io/theorem_proving_in_lean4/
- Benjamin C. Pierce. Types and Programming Languages. MIT Press, 2002.
- Lean 4 Web 编辑器:https://live.lean-lang.org/
- Lean 社区主页:https://leanprover-community.github.io/






