11. norm_num:数值计算框架
本章目标:理解 norm_num 的插件化架构与证明策略,学会编写自定义插件,
掌握它与 simp、decide、omega 的边界。
11.1. norm_num 解决什么问题
norm_num 证明具体数值的命题——等式、不等式、整除性、素性等:
-- 四则运算 #check (by norm_num : (2 : ℝ) + 3 = 5) -- ⊢ 判定结果为 True,内核验证通过 -- 素性判定 #check (by norm_num : Nat.Prime 127) -- 内部调用 Miller-Rabin 风格的确定性素性测试 -- 整除 #check (by norm_num : (10 : ℤ) ∣ 1000) -- 有理数比较 #check (by norm_num : (3 : ℚ) / 4 < 1)
关键区分:decide 把命题归约到 Bool 并让 kernel 枚举;
norm_num 在 meta 层 做算术,然后构造一个让 kernel 验证的证明项。
二者的性能差异在大数上极为明显:
-- decide 会超时:内核需要展开 ~2^31 步 -- #check (by decide : Nat.Prime 2147483647) -- timeout -- norm_num 秒出:meta 层做确定性素性测试,构造证书 #check (by norm_num : Nat.Prime 2147483647)
11.2. 核心架构:插件注册表
norm_num 的设计是一个可扩展的插件注册表。
每个插件负责处理以特定 head symbol 开头的表达式。
11.2.1. Result 类型
插件的返回类型 NormNum.Result 有三个构造子。
下面是简化后的核心形状,省略了部分字段;完整定义见 Mathlib.Tactic.NormNum.Core:
inductive Result where
- isNat (inst : Expr) (lit : Expr) (proof : Expr)
-- 表达式 = Nat.cast lit,proof : e = Nat.cast lit
- isNegNat (inst : Expr) (lit : Expr) (proof : Expr)
-- 表达式 = -Nat.cast lit
- isBool (val : Bool) (proof : Expr)
-- 命题为 True/False
isNat 和 isNegNat 把任意数值表达式规范化为自然数字面量或其负值,
附带一个证明该等式的 proof。isBool 用于布尔判定(如素性)。
11.2.2. 插件注册
用 @[norm_num <pattern>] attribute 注册插件:
-- pattern 指定该插件能处理的 head symbol
@[norm_num Nat.Prime _]
def evalPrime : NormNumExt where
eval {u α} (e : Expr) : SimpM (Option Result) := do
-- e 是待规范化的表达式
-- 返回 some result 表示成功,none 表示该插件不处理
...
norm_num 收到一个目标时的分派流程:
-
提取目标表达式的 head symbol
-
在注册表中查找匹配的插件
-
依次尝试匹配的插件,直到某个返回
some -
用返回的
Result构造最终证明
11.2.3. 内置插件清单
-
插件:
evalAdd—— 处理的 head symbol:HAdd.hAdd—— 示例:2 + 3 = 5 -
插件:
evalMul—— 处理的 head symbol:HMul.hMul—— 示例:6 * 7 = 42 -
插件:
evalPow—— 处理的 head symbol:HPow.hPow—— 示例:2 ^ 10 = 1024 -
插件:
evalDiv—— 处理的 head symbol:HDiv.hDiv—— 示例:(10 : ℚ) / 3 -
插件:
evalMod—— 处理的 head symbol:HMod.hMod—— 示例:17 % 5 = 2 -
插件:
evalPrime—— 处理的 head symbol:Nat.Prime—— 示例:Nat.Prime 127 -
插件:
evalGcd—— 处理的 head symbol:Nat.gcd—— 示例:Nat.gcd 12 8 = 4 -
插件:
evalDvd—— 处理的 head symbol:Dvd.dvd—— 示例:(3 : ℤ) ∣ 12 -
插件:
evalLe—— 处理的 head symbol:LE.le—— 示例:(2 : ℝ) ≤ 3 -
插件:
evalLt—— 处理的 head symbol:LT.lt—— 示例:(1 : ℚ) < 2
11.3. 证明构造策略
norm_num 的核心思想:在 meta 层做计算,在 object 层给出证书。
11.3.1. 加法示例
证明 37 + 45 = 82:
-- meta 层:Lean 原生 Nat 加法,瞬间得到 82 -- object 层:构造如下证明项 -- norm_num 不是直接 rfl——它把大数拆成 kernel 容易验证的步骤 -- 内部大致流程: -- 1. 37 和 45 都已是 Nat literal,无需进一步规范化 -- 2. meta 层计算 37 + 45 = 82 -- 3. 构造 proof : 37 + 45 = 82 -- 利用 Nat literal 的 kernel reduction 特性 -- kernel 验证 isNatLit 37 + isNatLit 45 →β isNatLit 82 ✓
11.3.2. 素性示例
证明 Nat.Prime 127:
-- meta 层: -- 1. 检查 127 > 1 -- 2. 试除 2, 3, 5, 7, 11(≤ √127 ≈ 11.3) -- 3. 确认无因子 -- object 层: -- 构造 minFac 证书:minFac 127 = 127 -- 这比让 kernel 枚举所有可能的因子快得多 -- kernel 只需验证证书的正确性,不需要自己搜索
11.3.3. 与 kernel computation 的对比
-
计算位置 ——
norm_num:meta 层(elaborator) ——decide/native_decide:kernel / 编译器 -
输出 ——
norm_num:证明项(证书) ——decide/native_decide:rfl : decide p = true -
大数性能 ——
norm_num:优秀 ——decide/native_decide:可能超时 -
可信基 ——
norm_num:kernel 验证证书 ——decide/native_decide:信任 kernel reduction / native code -
可扩展性 ——
norm_num:插件系统 ——decide/native_decide:需要Decidable实例
11.4. 编写自定义插件
11.4.1. 插件的结构
一个 norm_num 插件只需做三件事:模式匹配、meta 层计算、构造证明。
下面用伪代码展示骨架——不涉及 Qq 等元编程细节:
@[norm_num MyFunc _] -- ❶ 注册:声明处理哪个 head symbol
def evalMyFunc : NormNumExt where
eval (e : Expr) := do
-- ❷ 模式匹配:提取 MyFunc 的参数
let arg ← 从 e 中提取参数,失败则 return none
-- ❸ meta 层计算:用 Lean 原生函数得到结果
let result := myFuncImpl arg
-- ❹ 构造证明:给 kernel 一个可验证的证书
let proof ← 构造 "MyFunc arg = result" 的证明项
return some (.isNat ... result proof)
关键设计规则:当插件无法处理当前表达式时,返回
none而非抛出
failure。none让norm_num继续尝试其他插件;failure会中断
整个 pipeline,导致本可由其他插件处理的目标也一起失败。
完整的可编译示例(为 Nat.fib 编写插件)留作练习 10.4;
它需要用到 NormNumExt、Qq quoted expression 和 proof construction,
属于进阶元编程内容。
11.4.2. 插件开发要点
模式匹配要精确:@[norm_num Nat.fib _] 中的 pattern 决定何时触发。
写错 pattern 会导致插件永远不被调用,且无任何报错。
证明构造是难点:meta 层计算很容易,但构造让 kernel 接受的证明项需要 对 Lean 的类型系统和 kernel reduction 规则有深入理解。 常用策略:
-
小范围:直接用
mkDecideProof,让 kernel reduce -
大范围:构造分步证书(如素性的 minFac 证书)
-
通用:用
Qqquoted expression 确保类型安全
11.5. norm_num 与 simp 的配合
11.5.1. 作为 simp 的 discharger
simp 在化简条件引理时可以调用 norm_num 来 discharge 数值侧条件:
-- 引理 div_add_mod 有侧条件 b ≠ 0 -- simp 在应用该引理时需要证明 3 ≠ 0 -- norm_num 作为 discharger 自动处理 example (n : ℤ) : n % 3 + 3 * (n / 3) = n := by simp [Int.emod_add_ediv] -- simp 内部:需要 (3 : ℤ) ≠ 0 → norm_num discharge ✓
11.5.2. 管线组合
常见的组合模式:
-- 模式 1:simp 化简结构,norm_num 收尾数值
example : (2 : ℝ) * 3 + 1 = 7 := by
norm_num
-- norm_num 单独即可,不需要 simp
-- 模式 2:simp 展开定义后 norm_num 计算
example : Nat.factorial 5 = 120 := by
simp [Nat.factorial]
-- 展开 factorial 定义
norm_num
-- 计算具体值
-- 模式 3:norm_num 作为 simp extension
-- 在 simp set 中启用 norm_num
example : (2 : ℝ) + 3 ≠ 6 := by
simp (config := { decide := false }) only []
norm_num
11.6. 失败模式与诊断
11.6.1. 含变量的表达式
-- ✗ norm_num 无法处理含自由变量的表达式 example (x : ℝ) : x + 0 = x := by norm_num -- 失败!x 不是具体数值 -- 应使用:simp 或 ring
norm_num 只处理完全具体的数值表达式。
一旦出现自由变量,它会返回 none,tactic 报错。
11.6.2. 缺少插件
-- ✗ 没有处理 Nat.sqrt 的内置插件 -- example : Nat.sqrt 49 = 7 := by norm_num -- 失败 -- 解法 1:展开定义后用其他 tactic example : Nat.sqrt 49 = 7 := by native_decide -- 解法 2:编写自定义插件(见 10.4 节)
当 norm_num 失败时,检查目标的 head symbol 是否有对应插件。
set_option trace.Meta.NormNum true 可以查看插件分派过程。
11.6.3. 类型不匹配
-- ✗ 默认数值类型是 Nat,Nat 上没有减法负数 -- example : 3 - 5 = -2 := by norm_num -- 类型错误:Nat 上 3 - 5 = 0(截断减法) -- ✓ 指定整数类型 example : (3 : ℤ) - 5 = -2 := by norm_num
norm_num 对类型敏感。确保数值表达式在正确的类型上工作。
11.6.4. 诊断工具
-- 查看 norm_num 的插件分派和执行过程 set_option trace.Meta.NormNum true in example : (2 : ℝ) + 3 = 5 := by norm_num
11.7. 适用范围速查
-
目标形式:
2 + 3 = 5—— 推荐 tactic:norm_num—— 原因:具体数值等式 -
目标形式:
Nat.Prime 127—— 推荐 tactic:norm_num—— 原因:有内置素性插件 -
目标形式:
(3/4 : ℚ) * 4 = 3—— 推荐 tactic:norm_num—— 原因:有理数算术 -
目标形式:
x + 0 = x—— 推荐 tactic:simp/ring—— 原因:含变量 -
目标形式:
x + 1 > x—— 推荐 tactic:omega/linarith—— 原因:线性不等式 -
目标形式:
Nat.Prime 2147483647—— 推荐 tactic:norm_num—— 原因:大素数,meta 层计算 -
目标形式:
2 ^ 32 = 4294967296—— 推荐 tactic:norm_num—— 原因:幂运算 -
目标形式:
n % 2 = 0 ∨ n % 2 = 1—— 推荐 tactic:omega—— 原因:含变量的模算术 -
目标形式:
Nat.sqrt 49 = 7—— 推荐 tactic:native_decide—— 原因:无norm_num插件
经验法则:目标中没有自由变量且涉及算术 → 先试 norm_num。
11.8. 与其他 tactic 的边界
omega 处理线性整数算术(Presburger arithmetic),可以含变量;
ring 证明交换环上的多项式恒等式,可以含变量但只处理等式;
decide 通过 kernel reduction 判定,小规模可用,大数超时。
example (n : Nat) : n + 0 = n := by omega -- 含变量,norm_num 不行 example (x : ℝ) : (x + 1) ^ 2 = x ^ 2 + 2*x + 1 := by ring -- 多项式恒等式 example : (3 : ℝ) < 4 := by norm_num -- 具体不等式,ring 不行 example : Nat.Prime 104729 := by norm_num -- 大素数,decide 超时
11.9. 练习
练习 10.1(基础):用 norm_num 证明以下命题。
-- (a) 有理数算术 example : (7 : ℚ) / 3 + 5 / 3 = 4 := by sorry -- (b) 整数整除 example : (15 : ℤ) ∣ 450 := by sorry -- (c) 素性 example : Nat.Prime 1009 := by sorry
练习 10.2(诊断):解释为什么以下 norm_num 调用失败,并给出正确的 tactic。
-- (a) example (n : Nat) : n * 1 = n := by norm_num -- 为什么失败? -- (b) example : (5 : Nat) - 8 = 0 := by norm_num -- 能成功吗? -- (c) example : Nat.sqrt 100 = 10 := by norm_num -- 为什么失败?
练习 10.3(组合):选择合适的 tactic 组合证明以下命题。
-- (a) example : Nat.factorial 6 = 720 := by sorry -- (b) example (n : ℤ) (h : n > 0) : n + 3 > 2 := by sorry -- (c) example : (2 : ℝ) ^ 8 - 1 = 255 := by sorry
练习 10.4(进阶):为 Nat.fib 编写一个 norm_num 插件,
使得 by norm_num : Nat.fib 10 = 55 能够通过。
提示:参考 10.4.1 节的框架,重点在证明构造部分。
11.10. 小结
-
定位:具体数值命题的快速证明,无自由变量 -
架构:插件注册表,@[norm_num head _]注册 -
证明策略:meta 层计算 + 构造 kernel 可验证的证书 -
vs decide:meta 计算 vs kernel reduction,大数性能差异巨大
-
vs omega:
norm_num不含变量;omega处理线性整数算术 -
vs ring:
norm_num处理具体数值;ring处理多项式恒等式 -
与 simp 配合:作为 discharger 处理数值侧条件
-
扩展:实现
NormNumExt,返回Result,注册到 head symbol -
失败诊断:
trace.Meta.NormNum查看分派过程
下一章我们将学习 aesop——一个可配置的前向/后向搜索引擎。