虽然说自己已经可以用lean4写一些数学证明了,但是关于lean4语言本身还缺乏一些基础的认识。本文是与Gemini 3对话的内容,受益良多。
在Lean4中命题即类型,证明命题即构造具有对应类型的项(柯里-霍华德同构)。如何理解
这是一个非常深刻且美妙的概念。理解“命题即类型(Propositions as Types)”和“证明即程序(Proofs as Programs)”是掌握 Lean 4 的核心。
简单来说,在 Lean 中,写数学证明和写计算机程序本质上是同一件事。
我们可以通过以下几个层次来拆解这个概念(即柯里-霍华德同构 Curry-Howard Isomorphism):
1. 核心类比表
首先,我们需要建立一个心理映射表:
| 逻辑/数学领域 (Logic) | 编程/类型论领域 (Programming) | Lean 4 中的体现 |
|---|---|---|
| 命题 ($P$) | 类型 ($T$) | 1 + 1 = 2 是一个类型 |
| 证明 ($p$) | 该类型的一个实例/值 ($t$) | rfl 是上述类型的一个值 |
| 蕴含 ($P \to Q$) | 函数 (P -> Q) |
一个接收 $P$ 并返回 $Q$ 的函数 |
| 全称量词 ($\forall x, P(x)$) | 依赖函数 (Dependent Function) | 接收参数 $x$,返回类型随 $x$ 变化的函数 |
| 且 ($P \land Q$) | 积类型/结构体 (Product/Struct) | 包含两个字段的结构体:(proof_of_P, proof_of_Q) |
| 或 ($P \lor Q$) | 和类型/枚举 (Sum/Enum) | 或者是 $P$ 的值,或者是 $Q$ 的值 |
| 假 ($\text{False}$) | 空类型 (Empty Type) | 一个没有任何值的类型 (Void) |
2. 详细解读
第一层:命题是什么?(Propositions as Types)
在传统编程中,类型通常是 Int, String, List。
在 Lean 中,数学陈述也是一种类型。
- 例子:
1 + 1 = 2在 Lean 中,这不只是一个布尔值(True/False),而是一个类型(Type)。- 如果这个命题是真的,那么这个类型里就存在至少一个“值”(term)。
- 如果这个命题是假的(比如
1 + 1 = 3),那么这个类型就是空的,你造不出任何属于这个类型的值。
第二层:证明是什么?(Proofs as Terms)
要证明一个命题,你只需要构造出属于该类型的一个值。
- 代码示例:
当你写下
1 2 3 4 5 6-- 定义一个类型(命题):p 是 "1 + 1 = 2" def p : Prop := 1 + 1 = 2 -- 提供一个值(证明):proof_of_p 是类型 p 的一个实例 -- rfl 是 "reflexivity"(自反性)的缩写,是证明等式的基本构造子 def proof_of_p : p := rfldef或theorem时,编译器(Type Checker)会检查你等号右边的“值”是否真的属于冒号右边的“类型”。如果检查通过,证明就成立了。
第三层:蕴含即函数(Implication as Function)
这是最精彩的部分。逻辑上的“如果 A 成立,那么 B 成立”($A \to B$),在编程中就是一个函数。
-
逻辑角度:给我一个 A 的证明,我就能推导出 B 的证明。
-
编程角度:给我一个类型为
A的参数,我就返回一个类型为B的值。 -
代码示例:
1 2 3 4 5 6 7-- 假设 P 和 Q 是两个命题 variable (P Q : Prop) -- 这是一个证明:证明 "如果 P 且 Q 为真,那么 P 为真" -- 输入参数 h 的类型是 (P ∧ Q),返回值类型是 P theorem extract_p (h : P ∧ Q) : P := h.left -- 我们从结构体 h 中取出了左边的字段这完全等同于写一个函数,输入一个 Pair,返回 Pair 的第一个元素。
第四层:全称量词与依赖类型($\forall$ as Dependent Function)
全称量词 $\forall x, P(x)$ 的意思是:对于任意的 $x$,命题 $P(x)$ 都成立。
在编程中,这对应依赖函数(Dependent Function)。这是一种特殊的函数,其返回值的类型取决于输入参数的值。
- 代码示例:
调用
1 2 3 4-- 证明:对于任意自然数 n,n = n -- 这是一个函数,输入一个具体的数字 n,返回 "n = n" 这个类型的证明 theorem self_equal (n : Nat) : n = n := rflself_equal 5会得到一个类型为5 = 5的证明(值)。
3. 一个完整的“编程即证明”演示
让我们来看看著名的 Modus Ponens(肯定前件律): 逻辑表述:如果已知 $P \to Q$ 且已知 $P$,则可以推出 $Q$。
在 Lean 中,这只是简单的函数调用:
|
|
理解关键: 你不是在“寻找”真理,你是在“组装”数据。
- 既然
h1是一个把 $P$ 变 $Q$ 的转换器; h2是原料 $P$;- 那么
h1 h2自然就是产品 $Q$。
4. Prop 与 Type 的区别
虽然我们说命题即类型,但在 Lean 4 中,它们处于不同的“宇宙”层级:
- Type (Sort 1): 用于数据和计算。比如
Nat,List Int。我们要区分0和1,因为它们在计算上是不同的数据。 - Prop (Sort 0): 用于逻辑命题。比如
1 = 1,2 > 1。
为什么区分?(证明无关性 Proof Irrelevance)
- 对于数据(Type),我们关心它是哪个值(
3和4是不同的)。 - 对于命题(Prop),我们只关心它是否有证明,而不关心证明长什么样。
- 例如:证明
2 + 2 = 4,你可以用手指头数,也可以用皮亚诺公理推导。在 Lean 眼里,这两个证明是等价的,只要证明了它是真的,它们就是相等的。运行时,Prop 类型的值会被擦除(即不需要占用内存),因为程序跑起来只需要知道逻辑是对的,不需要带着证明跑。
- 例如:证明
总结
在 Lean 4 中理解这句话,你需要转变思维:
- 不要把证明看作文本推导,而要看作拼积木(构造数据)。
- 定理(Theorem) 就是 函数签名(Function Signature)。
- 证明过程(Proof) 就是 函数体(Function Body)。
- Q.E.D. 就是 编译通过(Type Check Passed)。
当你被要求证明 $A \to B$ 时,不要想“为什么 A 能推出 B”,而要想“我手里有个 A 类型的数据,怎么把它加工成 B 类型的数据?”
继续讲讲“宇宙”层级
这是一个非常重要的问题。如果说“命题即类型”是 Lean 的骨架,那么**宇宙(Universes)**就是防止这个骨架坍塌的地基。
在 Lean 中,你可能会经常看到编译报错提到 Type u、Sort u 或者 universe level,这就是涉及到宇宙层级了。
我们通过三个问题来理解它:是什么?为什么?怎么用?
1. 什么是宇宙(Universe)?
回想一下我们之前的定义:
5是一个值,它的类型是Nat。rfl是一个证明,它的类型是1 = 1。
那么问题来了:Nat 的类型是什么?1 = 1 的类型又是什么?
在 Lean 中,类型本身也是一种“东西”,所以类型也有类型。我们把“类型的类型”称为 宇宙(Universe)。
Lean 的世界是一个无限向上的阶梯:
第 0 级:值 (Terms)
这是具体的数值或证明对象。
5"Hello"[](空列表)
第 1 级:类型 (Types)
这是第 0 级东西的容器。
Nat(包含 5)String(包含 “Hello”)List Nat(包含 [])
第 2 级:类型的类型 (Type of Types)
这是第 1 级东西的容器。
- 在 Lean 中,
Nat的类型叫做Type(实际上是Type 0)。
第 3 级及以上
Type 0的类型是Type 1。Type 1的类型是Type 2。- …以此类推,直到无穷。
用代码验证一下(你可以在 VS Code 中输入 #check 查看):
|
|
2. Prop 和 Type:两个平行的起点
虽然阶梯是无限向上的,但 Lean 的底层(地基)其实分成了两支。Lean 内部使用关键字 Sort 来统一表示这两个分支。
分支一:逻辑世界 (Prop / Sort 0)
- 定义:这是所有命题居住的宇宙。
- 别名:
Prop实际上就是Sort 0的语法糖。 - 特点:
1 = 1的类型是Prop。2 > 3的类型是Prop。- 证明无关性:在这个层级里,我们不关心“值”(证明)长什么样,只关心有没有。
- 运行时擦除:编译成二进制程序时,
Prop里的所有内容都会被删掉,不占空间。
分支二:数据世界 (Type / Sort 1, Sort 2…)
- 定义:这是所有数据结构居住的宇宙。
- 别名:
Type 0是Sort 1的语法糖(通常简写为Type)。Type 1是Sort 2的语法糖。Type u是Sort (u+1)的语法糖。
- 特点:
Nat、Bool、List都在这里。这里的“值”对计算结果很重要,不能删。
3. 为什么要分层级?(罗素悖论)
你可能会问:“为什么要搞这么多 Type 0, Type 1… 为什么不直接规定 Type 的类型就是 Type 自己呢?”
如果允许 Type : Type(类型包含其自身),数学系统就会崩溃。这对应著名的 罗素悖论 (Russell’s Paradox)。
通俗解释: 想象有一本“书目大全”,它列出了图书馆里所有的书。
- 问题:这就引出了一个悖论,这本“书目大全”应该把它自己列进去吗?
- 如果允许“集合包含自身”,你就可以构造一个“所有不包含自己的集合的集合”。这就导致了逻辑矛盾(如果是,则不是;如果不是,则是)。
为了避免这个逻辑炸弹,Lean(以及现代类型论)强制规定: 容器必须比它里面的东西高一级。
Nat在Type 0里。- 包含
Nat的List(作为一个类型构造器)也通常在Type 0层面操作。 - 但是,如果你想定义一个包含“所有类型”的列表
[Nat, Bool, String],这个列表本身的类型就不能是Type 0了,它必须升级到Type 1。
4. 宇宙多态 (Universe Polymorphism)
在写代码时,为了避免麻烦,我们不想为 Type 0 写一个 List,又为 Type 1 写一个 List。我们希望代码是通用的。
这就有了 宇宙变量 (Universe Variables),通常写作 u, v。
当你查看 Lean 标准库中 List 的定义时,你会发现它是这样写的(简化版):
|
|
这意味着:
- 如果
α是Nat(Type 0),那么List Nat也是Type 0。 - 如果
α是Type(Type 1),那么List Type就是Type 1。
这就是“宇宙多态”:你的函数或类型可以在任意层级的宇宙上工作。
5. 总结:如何理解这张图谱
把 Lean 的类型系统想象成一个巨大的公寓楼:
-
地下室 (
Sort 0/Prop):- 住着:
1=1,Exists x, ...等命题。 - 特点:这层楼也是一种类型,但这里的东西主要用于“验证”,不参与实际的“物理构建”(运行时被擦除)。
- 住着:
-
一楼 (
Sort 1/Type 0):- 住着:
Nat,String,Bool等常见数据类型。 - 这是我们写普通程序最常待的地方。
- 住着:
-
二楼 (
Sort 2/Type 1):- 住着:
Type 0本身,以及包含了一楼住户的那些“元类型”(比如List Type)。
- 住着:
-
顶楼 (
Type u):- 是一个变量,允许你编写同一种逻辑,适配每一层楼。
一句话心法:
当你写代码时,如果是普通数据,就在 Type;如果是逻辑证明,就在 Prop。只有当你在玩弄“类型本身”(比如写元编程,或者定义极其抽象的范畴论结构)时,才需要关心 Type 1 及以上的层级。
讲讲tactics
如果说“命题即类型”是 Lean 的物理定律(底层原理),那么 Tactics(策略) 就是 Lean 的工程脚手架(高层工具)。
到目前为止,我们一直在说“证明就是写程序”。但在实际数学中,直接写出那个最终的“程序(Term)”通常是非常困难且反直觉的。
Tactics 的出现,是为了让我们能用指令式的方法,一步步地指导 Lean 构造出那个复杂的证明项。
1. 为什么我们需要 Tactics?
假设你要证明一个复杂的数学定理。
-
Term Mode(项模式/硬写代码): 就像你必须一次性把整个复杂的函数体写出来,不能有任何语法错误,脑子里要以此维护所有的变量状态。这就像用汇编语言写大型软件。
- 优点:精确,底层。
- 缺点:对于长证明,人类的大脑很难直接构造出那个嵌套了十几层的 Lambda 表达式。
-
Tactic Mode(策略模式/交互式): Lean 提供了一个交互环境。你不需要直接给出答案,而是告诉 Lean:“先处理这个假设”,“把那个定理套用一下”,“把等式左边化简一下”。Lean 会实时反馈你现在的状态。
- 优点:交互式,不仅是验证,更是辅助探索。分解问题,各个击破。
2. 核心机制:Tactic Mode 是如何工作的?
在 Lean 中,当你输入关键字 by 时,你就从“写代码模式”切换到了“策略模式”。
心理模型:填坑游戏
-
目标(Goal): Lean 会在这个类型的定义处挖一个坑(Hole,学名 Metavariable)。你的任务就是填上这个坑。
-
状态(Tactic State): VS Code 右侧会显示当前的证明状态。它长这样:
1 2 3h1 : P -- 你的手牌(上下文/假设) h2 : P → Q ⊢ Q -- 你的目标(要构造的类型)⊢(Turnstile) 符号:左边是你拥有的,右边是你需要的。 -
指令(Commands): 你每输入一个 tactic(如
intro,apply),Lean 就会根据指令修改当前的 State,直到目标被解决。 -
幕后(Elaboration): 当你写完
by ...块时,Lean 的编译器(Elaborator)会自动把你的这一堆指令翻译回底层的 Lambda 表达式(Term)。机器只认 Term,Tactics 是给人用的。
3. 常见 Tactics 与底层 Term 的对应关系
理解 Tactics 的最好方法,是看它们对应什么底层代码(柯里-霍华德同构的体现)。
假设我们要证明 $P \to Q \to P$。
场景 1:引入假设 (intro)
- Tactic:
intro h - 对应 Term: Lambda 抽象
fun h => ... - 作用:如果目标是
P → Q,intro h会把P移到左边的上下文里,把目标变成Q。- 直觉:为了证明“如果 P 则 Q”,我们先假设 P 成立(记为 h),然后去证明 Q。
场景 2:应用定理 (apply)
- Tactic:
apply f - 对应 Term: 函数调用
f ?(待定参数) - 作用:逆向推理。如果目标是
Q,而你有一个函数f : P → Q,apply f会把目标变成P。- 直觉:为了得到 Q,因为我知道 f 能推出 Q,所以我只要能搞定 f 需要的原料 P 就行了。
场景 3:精确匹配 (exact)
- Tactic:
exact h - 对应 Term: 直接填入值
h - 作用:如果目标是
P,而你手头正好有一个h : P,你就大喊exact h,证明结束。
场景 4:结构分解 (cases / rcases)
- Tactic:
cases h - 对应 Term: 模式匹配
match h with ... - 作用:如果你有一个“或”($A \lor B$)或者“且”($A \land B$)类型的假设,把它拆解开。
4. 实例演示:两种模式的对比
让我们证明:如果 P 且 Q 成立,那么 P 成立。
类型为:P ∧ Q → P
方式 A:Term Mode (直接构造)
你需要非常清楚 Pair 类型的内部结构。
|
|
方式 B:Tactic Mode (交互式)
这是更像“数学证明过程”的写法。
|
|
发生了什么?
Lean 实际上把你写的那堆 by ... 代码,悄悄编译成了:
fun h => match h with | intro hp hq => hp
这和方式 A 的本质是一样的,但方式 B 每一步都有提示。
5. 高级 Tactics:自动化武器
Lean 4 的强大之处在于它不仅有手动挡(intro, apply),还有自动挡。因为 Tactics 本质上是 Lean 的元程序(Metaprograms),它们可以执行搜索算法。
rw(rewrite): 等量代换。如果你知道a = b,可以用它把所有的a换成b。这在底层对应复杂的等式传输原理(Eq.subst),手写非常痛苦,用rw极其爽快。simp(simplify): 简化器。它是 Lean 的扫地机器人。它有一个巨大的数据库,里面存了成千上万条化简规则(比如x + 0 = x,a ∧ True ↔ a)。你输入simp,它就自动尝试化简目标。linarith: 线性算术求解器。如果你有x < y和y < z,目标是x < z,你不需要手动引用传递性,直接输入linarith,它会自动搜索不等式证明。
6. 总结:如何看待 Tactics
- 思维的缓冲:Term Mode 是“自顶向下”的构造(必须先想好整体结构),Tactic Mode 允许“自底向上”或“逆向”的探索。
- 可读性权衡:
- Term Mode 的代码短,但难懂(全是符号)。
- Tactic Mode 的代码长,但记录了思维过程(先引入,再拆解,再应用…)。
- Lean 4 的特色:在 Lean 4 中,Tactics 本身也是用 Lean 语言写的!这意味着你可以自己写 Tactics 来帮自己证明。
一句话总结: Term 是汇编指令,Tactic 是高级语言脚本。 我们用 Tactic 指挥 Lean 去生成那些繁琐的 Term,从而完成柯里-霍华德同构要求的“构造证明值”的任务。