Featured image of post 基于Gemini 3的Lean4的一些理解

基于Gemini 3的Lean4的一些理解

虽然说自己已经可以用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 := rfl
    
    当你写下 deftheorem 时,编译器(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 :=
      rfl
    
    调用 self_equal 5 会得到一个类型为 5 = 5 的证明(值)。

3. 一个完整的“编程即证明”演示

让我们来看看著名的 Modus Ponens(肯定前件律): 逻辑表述:如果已知 $P \to Q$ 且已知 $P$,则可以推出 $Q$。

在 Lean 中,这只是简单的函数调用

1
2
3
4
5
6
7
8
variable (P Q : Prop)

-- mp 是函数名
-- h1 是一个函数 (P -> Q)
-- h2 是一个值 (P)
-- 我们的目标是构造一个 Q 类型的值
def mp (h1 : P  Q) (h2 : P) : Q :=
  h1 h2  -- 将参数 h2 传给函数 h1,得到的结果就是 Q

理解关键: 你不是在“寻找”真理,你是在“组装”数据。

  • 既然 h1 是一个把 $P$ 变 $Q$ 的转换器;
  • h2 是原料 $P$;
  • 那么 h1 h2 自然就是产品 $Q$。

4. Prop 与 Type 的区别

虽然我们说命题即类型,但在 Lean 4 中,它们处于不同的“宇宙”层级:

  1. Type (Sort 1): 用于数据和计算。比如 Nat, List Int。我们要区分 01,因为它们在计算上是不同的数据。
  2. Prop (Sort 0): 用于逻辑命题。比如 1 = 1, 2 > 1

为什么区分?(证明无关性 Proof Irrelevance)

  • 对于数据(Type),我们关心它是哪个值34 是不同的)。
  • 对于命题(Prop),我们只关心它是否有证明,而不关心证明长什么样
    • 例如:证明 2 + 2 = 4,你可以用手指头数,也可以用皮亚诺公理推导。在 Lean 眼里,这两个证明是等价的,只要证明了它是真的,它们就是相等的。运行时,Prop 类型的值会被擦除(即不需要占用内存),因为程序跑起来只需要知道逻辑是对的,不需要带着证明跑。

总结

在 Lean 4 中理解这句话,你需要转变思维:

  1. 不要把证明看作文本推导,而要看作拼积木(构造数据)
  2. 定理(Theorem) 就是 函数签名(Function Signature)
  3. 证明过程(Proof) 就是 函数体(Function Body)
  4. Q.E.D. 就是 编译通过(Type Check Passed)

当你被要求证明 $A \to B$ 时,不要想“为什么 A 能推出 B”,而要想“我手里有个 A 类型的数据,怎么把它加工成 B 类型的数据?”

继续讲讲“宇宙”层级

这是一个非常重要的问题。如果说“命题即类型”是 Lean 的骨架,那么**宇宙(Universes)**就是防止这个骨架坍塌的地基。

在 Lean 中,你可能会经常看到编译报错提到 Type uSort 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 查看):

1
2
3
4
#check 5        -- 输出: 5 : Nat
#check Nat      -- 输出: Nat : Type
#check Type     -- 输出: Type : Type 1
#check Type 1   -- 输出: Type 1 : Type 2

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 0Sort 1 的语法糖(通常简写为 Type)。
    • Type 1Sort 2 的语法糖。
    • Type uSort (u+1) 的语法糖。
  • 特点NatBoolList 都在这里。这里的“值”对计算结果很重要,不能删。

3. 为什么要分层级?(罗素悖论)

你可能会问:“为什么要搞这么多 Type 0, Type 1… 为什么不直接规定 Type 的类型就是 Type 自己呢?”

如果允许 Type : Type(类型包含其自身),数学系统就会崩溃。这对应著名的 罗素悖论 (Russell’s Paradox)

通俗解释: 想象有一本“书目大全”,它列出了图书馆里所有的书。

  • 问题:这就引出了一个悖论,这本“书目大全”应该把它自己列进去吗?
  • 如果允许“集合包含自身”,你就可以构造一个“所有不包含自己的集合的集合”。这就导致了逻辑矛盾(如果是,则不是;如果不是,则是)。

为了避免这个逻辑炸弹,Lean(以及现代类型论)强制规定: 容器必须比它里面的东西高一级。

  • NatType 0 里。
  • 包含 NatList(作为一个类型构造器)也通常在 Type 0 层面操作。
  • 但是,如果你想定义一个包含“所有类型”的列表 [Nat, Bool, String],这个列表本身的类型就不能是 Type 0 了,它必须升级到 Type 1

4. 宇宙多态 (Universe Polymorphism)

在写代码时,为了避免麻烦,我们不想为 Type 0 写一个 List,又为 Type 1 写一个 List。我们希望代码是通用的。

这就有了 宇宙变量 (Universe Variables),通常写作 u, v

当你查看 Lean 标准库中 List 的定义时,你会发现它是这样写的(简化版):

1
2
3
4
5
6
7
8
9
-- u 是一个占位符,可以是 0, 1, 2...
universe u

-- List 接受一个类型 α
-- 这个 α 位于宇宙 u 中 (Type u)
-- 返回的 List α 也位于宇宙 u 中
inductive List (α : Type u) : Type u where
  | nil : List α
  | cons : α  List α  List α

这意味着:

  1. 如果 αNat (Type 0),那么 List Nat 也是 Type 0
  2. 如果 α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 时,你就从“写代码模式”切换到了“策略模式”。

心理模型:填坑游戏

  1. 目标(Goal): Lean 会在这个类型的定义处挖一个坑(Hole,学名 Metavariable)。你的任务就是填上这个坑。

  2. 状态(Tactic State): VS Code 右侧会显示当前的证明状态。它长这样:

    1
    2
    3
    
    h1 : P      -- 你的手牌(上下文/假设)
    h2 : P → Q
    ⊢ Q         -- 你的目标(要构造的类型)
    

    (Turnstile) 符号:左边是你拥有的,右边是你需要的。

  3. 指令(Commands): 你每输入一个 tactic(如 intro, apply),Lean 就会根据指令修改当前的 State,直到目标被解决。

  4. 幕后(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 → Qintro h 会把 P 移到左边的上下文里,把目标变成 Q
    • 直觉:为了证明“如果 P 则 Q”,我们先假设 P 成立(记为 h),然后去证明 Q。

场景 2:应用定理 (apply)

  • Tactic: apply f
  • 对应 Term: 函数调用 f ? (待定参数)
  • 作用逆向推理。如果目标是 Q,而你有一个函数 f : P → Qapply 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 类型的内部结构。

1
2
3
example (P Q : Prop) : P  Q  P :=
  fun h => h.left
-- 解释:这是一个匿名函数,接收 h,返回 h 的左分量

方式 B:Tactic Mode (交互式)

这是更像“数学证明过程”的写法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
example (P Q : Prop) : P  Q  P := by
  -- 状态: ⊢ P ∧ Q → P
  intro h
  -- 状态: h : P ∧ Q 
  --       ⊢ P
  cases h with
  | intro hp hq =>
    -- 状态: hp : P, hq : Q
    --       ⊢ P
    exact hp
    -- 状态: No goals. 证明完成。

发生了什么? 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 < yy < z,目标是 x < z,你不需要手动引用传递性,直接输入 linarith,它会自动搜索不等式证明。

6. 总结:如何看待 Tactics

  1. 思维的缓冲:Term Mode 是“自顶向下”的构造(必须先想好整体结构),Tactic Mode 允许“自底向上”或“逆向”的探索。
  2. 可读性权衡
    • Term Mode 的代码短,但难懂(全是符号)。
    • Tactic Mode 的代码长,但记录了思维过程(先引入,再拆解,再应用…)。
  3. Lean 4 的特色:在 Lean 4 中,Tactics 本身也是用 Lean 语言写的!这意味着你可以自己写 Tactics 来帮自己证明。

一句话总结: Term 是汇编指令,Tactic 是高级语言脚本。 我们用 Tactic 指挥 Lean 去生成那些繁琐的 Term,从而完成柯里-霍华德同构要求的“构造证明值”的任务。

使用 Hugo 构建
主题 StackJimmy 设计