深入 Coq 09:归纳证明:自然数、列表、树
归纳证明是 Coq 中最核心的推理手段。无论是对自然数的算术性质、对列表的结构性质,还是对树的递归性质,归纳原理都以同一套框架给出严格证明。本文系统覆盖三类归纳:标准结构归纳、完全归纳(强归纳),以及依赖 Acc 和 Fix 的 well-founded 递归。
前置阅读:本系列 01(环境与项目结构)、02(目标窗口与 tactic 交互)、03(Gallina 核心语法)、04(基础 tactic 全景)提供了必要的语法和 tactic 背景。本文中出现的 induction、simpl、rewrite、auto 等 tactic 在 04 中均有详细说明。
自然数上的结构归纳
归纳原理的来源
nat 在 Coq 标准库中定义为:
1 | |
Inductive 指令自动生成归纳原理 nat_ind,其类型为:
1 | |
induction tactic 正是对这条原理的调用。证明目标 forall n, P n 时,induction n 将目标拆成两个子目标:P 0 和 P n -> P (S n)。
加法结合律的证明
1 | |
as [| a' IHa'] 是引入模式(intro pattern)。[| 左侧为 O 分支,右侧为 S 分支,a' 绑定前驱,IHa' 绑定归纳假设。这个写法在 04 中已经介绍,此处可以对比实际证明情形。
用 Show Proof. 可以看到归纳情形产生的项:
1 | |
这表明 induction 生成的证明项是对 nat_ind 的直接应用,IHa' 对应函数参数里的归纳假设。
引入模式的变体
不同的引入模式适用于不同场合:
1 | |
eqn:Hn 会在上下文中保留 Hn : n = 0 或 Hn : n = S n',当后续 rewrite 需要引用具体形式时很有用。
列表上的结构归纳
列表翻转的长度不变性
1 | |
app_length 是标准库引理 length (l1 ++ l2) = length l1 + length l2。omega 处理线性算术等式。
翻转两次等于原列表
1 | |
rev_app_distr 的类型为 rev (l1 ++ l2) = rev l2 ++ rev l1。这个引理在 05 中的 Search 演示里出现过,读者可以用 Search rev app 自行查找。
二叉树上的结构归纳
树的定义
1 | |
Coq 自动生成 tree_ind:
1 | |
树的镜像与大小
1 | |
as [| l IHl a r IHr] 把 Node 分支的两棵子树和两个归纳假设全部命名,避免 Coq 自动生成晦涩的名字。
1 | |
标准归纳卡住的情形与强归纳
卡住示例:Collatz 步骤上界
考虑一个简化问题:证明所有满足 n <= m 的自然数在某个谓词下成立,而谓词的递推关系依赖于 n/2 而非 n-1。标准 induction n 只能给出 P n -> P (S n) 形式的归纳假设,无法直接使用 P (n/2)。
一个更直接的展示场景:证明 Fibonacci 数满足某种下界。以标准归纳尝试下面的命题:
1 | |
证明 fib (n + 2) >= fib n + 1 时,induction n 在 S n' 情形给出的归纳假设是 fib (n' + 2) >= fib n' + 1,但目标需要用 fib (n' + 1 + 2) 和 fib (n' + 2 + 2) 的关系,跨度超出一步,归纳假设不够用。
实际上,Fibonacci 的典型归纳需要两步归纳假设(对 n 和 n+1 同时成立),这正是**完全归纳(strong induction)**的用武之地。
完全归纳的原理
Coq 标准库提供 lt_wf_ind(在 Coq.Arith.Wf_nat):
1 | |
这条原理说:若对任意 n,在所有小于 n 的数均满足 P 的前提下能证 P n,则 P 对所有自然数成立。
也可以自行定义等价的 strong_induction 引理并直接使用:
1 | |
用强归纳证明 Fibonacci 单调性
1 | |
归纳假设 IH : forall k, k < m -> 1 <= k -> fib k < fib (S k) 覆盖所有小于 m 的情形,因此对 m' 和 S m' 均可使用,而标准 induction 无法做到这一点。
Print Assumptions fib_lt. 输出:
1 | |
这表明整个证明只依赖 lt_wf(< 上的 well-founded 性),没有引入额外公理。
Well-Founded 递归与 Fix
Acc 与 well_founded 的定义
Coq 的 Acc(Accessibility)谓词定义在 Coq.Init.Wf:
1 | |
Acc R x 断言:x 关于关系 R 是可及的,即从 x 出发沿 R 无法无限下降。
1 | |
well_founded R 意味着 R 上不存在无穷下降链,这保证了沿 R 的递归一定终止。
Fix 的类型
1 | |
Fix 是 well-founded 递归的通用组合子。第一个参数是终止证据,第二个是递归步骤(形如 forall x, (forall y < x, P y) -> P x)。
用 Fix 定义欧几里得算法
1 | |
上面片段展示结构:rec 参数携带一个 m < n 的证明,保证递归调用仅用于更小的参数。实际可编译版本需要补充一些细节(如 k <> 0 的条件),此处意在说明 Fix 的使用模式。
在实践中,更常见的做法是使用 Program Fixpoint 或 Function,让 Coq 自动处理终止证明:
1 | |
{measure n} 告诉 Coq 用 n 作为递减量,Next Obligation 填充终止证明。
measure 与 wf 的关系
{measure f} 在展开后等价于 well_founded (fun x y => f x < f y)。标准库里对应 lt_wf 和 measure_wf:
1 | |
这意味着一切基于自然数度量的递归,其终止性最终归结于 lt_wf,而 lt_wf 本身在 Coq 的逻辑基础内可以证明(通过对 nat 的结构归纳)。
几个容易混淆的细节
induction 与 destruct 的区别
destruct n 对 n 做情形分析,产生 n = O 和 n = S n' 两个子目标,但不产生归纳假设。induction n 在 S n' 分支额外给出 IHn' : P n'。
若目标不依赖 n 的值而只依赖 n 的形状(如 n = 0 \/ n > 0),用 destruct 够了;只有命题需要对所有 n 递推时才用 induction。
归纳前的 generalize
一个常见错误:先用 intros 把某个变量引入上下文,再对另一个变量做归纳,导致归纳假设过弱。
1 | |
正确做法是在 induction 之前不引入 b,让 b 保留在目标中:
1 | |
此时归纳假设的形式为 forall b, a' + b = b + a',对所有 b 成立,足以推进 S a' 情形。
对嵌套归纳类型的 induction
当归纳类型含有嵌套(如树中每个节点存储一个列表),Coq 可能无法自动生成足够强的归纳原理。此时可以使用 Scheme 手动生成,或改用 size_ind(按大小归纳):
1 | |
然后对任意带大小函数的类型 T,通过 apply size_ind with (n := size_of t) 转化为自然数上的强归纳。
参考资料
- Coq 官方文档,Inductive Types 章节:https://coq.inria.fr/doc/v8.19/refman/language/core/inductive.html
- Software Foundations Vol. 1(Logical Foundations),Induction 章节
- Certified Programming with Dependent Types(CPDT),Adam Chlipala,General Recursion 章节
- Coq 标准库
Coq.Arith.Wf_nat:lt_wf_ind、measure_wf的定义与证明 - 本系列 04(基础 tactic 全景):
induction、destruct、rewrite的详细语义
练习 1:列表追加与长度
证明 forall (A : Type) (l1 l2 : list A), length (l1 ++ l2) = length l1 + length l2,不使用标准库中的 app_length,仅用 induction、simpl、reflexivity、omega。
练习 2:树的高度与大小
定义树的高度函数:
1 | |
证明 forall (A : Type) (t : tree A), size t <= 2 ^ height t - 1(可以先证 size t < 2 ^ (height t + 1),此形式更适合归纳推进)。
练习 3:用强归纳证明 Euclid 算法终止
不使用 Program Fixpoint,直接用 Fix lt_wf 定义一个对参数 a 递减的 gcd 变体,使得递归调用的第一参数严格小于当前第一参数(提示:Nat.mod_upper_bound 给出 a mod b < a 当 a > 0)。
