深入 Coq 13:证明红绿灯:常见卡住模式与诊断
证明中断不是随机的。Coq 报错信息有规律,每种卡住模式都有对应的诊断手段和修复路径。本文整理六类高频卡住模式,逐一给出错误现场、根因和处理方法,附诊断工具用法。
诊断工具速览
下列诊断命令不参与证明,只用于观察状态,本文各节均会用到。
1 | |
Set Printing All 是最常用的第一步:Coq 的记号、隐式参数和强制转换会把真实项目遮住,打开这个选项后目标才算"裸眼可读"。
模式一:目标中出现未预期的存在变量
错误现场
1 | |
存在变量(existential variable,evar)在目标或上下文中以 ?x、?Goal 的形式出现。某些情况下 Coq 不报错,而是让 evar 留在目标里,直到某个 tactic 试图匹配它才失败。
根因分析
evar 的主要来源有三个。
第一,eapply 留下了未解决的参数。eapply f 会为 f 的每个无法立即推断的参数生成 evar,这些 evar 必须在后续 tactic 中被实例化。
1 | |
第二,refine 留下了占位符。refine 允许在项中留 _,每个 _ 产生一个 evar 子目标。
第三,某些 tactic 组合在非预期的顺序下,产生了循环依赖的 evar,Coq 无法自动解开。
修复方法
用 instantiate 显式给出 evar 的值:
1 | |
或者改用 apply 加完整参数,完全避免 evar:
1 | |
诊断步骤:出现奇怪 evar 时,先执行 Show Proof. 查看当前 proof term,evar 在输出中以 ?evar_N 标注,结合上下文可以判断哪一步引入了它。
1 | |
模式二:宇宙不一致错误
错误现场
1 | |
或者更简短的版本:
1 | |
根因分析
Coq 的类型层级是 Prop : Type(0) : Type(1) : Type(2) : ...,Set 是 Type(0) 的别名(在某些模式下)。宇宙不一致的核心原因是试图把某个 Type(i) 的值放进需要 Type(j) 的位置,而 i ≥ j,违反了层级约束。
常见触发场景:
1 | |
另一个典型场景是多态函数被实例化到过高的层级:
1 | |
修复方法
打开宇宙层级显示,先观察:
1 | |
如果需要量化所有宇宙层级,使用 Universe Polymorphism:
1 | |
如果错误来自 Prop 和 Type 的混用,检查是否把 Prop 中的命题当作计算层的数据使用。Prop 的居民不能流入 Set/Type 的非 Prop 数据类型:
1 | |
实践中,宇宙不一致错误多出现在尝试定义涉及"类型的类型"的结构时。检查点是:Type 出现在等号右边的类型中,而不是参数类型中。
模式三:无法合一(Cannot unify)
错误现场
1 | |
根因分析
Cannot unify X with Y 背后通常有三种原因。
第一类是参数顺序错误,也是最常见的。某个函数的参数顺序与直觉不符:
1 | |
第二类是隐式参数不匹配。打开 Set Printing All 后才能看到真实的参数列表:
1 | |
隐式参数被自动推断时,如果上下文不够,推断结果可能与期望不符。用 @ 前缀可以显式指定所有参数:
1 | |
第三类是强制转换(coercion)隐藏了类型差异。Coq 会自动应用已注册的强制转换,这可能让目标看起来满足,实际上类型路径不同:
1 | |
修复方法
诊断的标准流程是:
Set Printing All展开所有记号和隐式参数;Check @f查看函数的完整签名;- 对比目标中的实际类型和 tactic 提供的类型,找到不匹配点;
- 用
change或convert_concl_no_check把目标改写成等价但更便于 tactic 匹配的形式。
1 | |
模式四:对错误变量做 induction 导致泛化不足
错误现场
证明没有报错,但 induction 完成后归纳假设里缺少必要的泛化,导致后续 tactic 无法完成:
1 | |
根因分析
对变量 n 做 induction n 之前,如果上下文中已经有其他变量 m 绑定在 n 上,归纳假设就不够强。经典场景:
1 | |
正确做法是在 induction 之前,把需要泛化的变量用 revert 放回目标,或者用 generalize dependent 直接泛化:
1 | |
或者使用 generalize dependent m 在 induction 之前把 m 泛化回目标:
1 | |
泛化的判断准则
一个实用的规则是:对 n 做 induction 之前,检查目标和上下文中是否有其他量词变量依赖 n,或者在递归步里需要对这些变量取不同值。如果有,就用 revert 或 generalize dependent 先把它们放回目标。
模式五:不透明定义阻塞化简
错误现场
1 | |
根因分析
Coq 中定义的透明度分三档:
Definition(透明):simpl和unfold都可以展开;Opaque:只能用于类型检查,不能被simpl/unfold展开;Qed结尾的引理:对外部是不透明的,simpl无法展开其定义体。
后一点是高频陷阱:用 Qed 封闭的证明,其 proof term 对后续证明不透明,只有类型(命题)可见。用 Defined 结尾的证明则是透明的,proof term 可被计算:
1 | |
修复方法
如果定义被标记为 Opaque 但需要展开,用 Transparent 临时解除:
1 | |
如果问题是 Qed 封闭的引理无法展开,有两条路:
- 把引理改为
Defined结尾(需要该引理的 proof term 有计算意义); - 不展开定义体,而是直接用引理本身做重写:
rewrite add_zero_qed。
About ident 会显示透明度信息:
1 | |
Print ident 则显示定义体本身,用于确认定义是否是预期的内容。
模式六:缺少 Require Import 导致 tactic 静默失败
错误现场
这一类没有明显错误信息,症状是 tactic 不报错但也不工作,或报出莫名其妙的找不到名字:
1 | |
根因分析
Coq 的标准库分模块,许多 tactic 和引理需要显式导入对应的库才可用。常见的遗漏:
| 功能 | 需要导入 |
|---|---|
lia(线性算术) |
Require Import Lia. |
omega(旧版线性算术) |
Require Import Omega.(8.14 后已弃用,改用 lia) |
ring、field |
Require Import Ring. 或 Require Import Field. |
auto 的扩展引理 |
视需要导入对应库 |
List 相关引理 |
Require Import List. Import ListNotations. |
Nat 模块 |
Require Import Arith. 或 Require Import PeanoNat. |
另一类静默失败来自 Hint 数据库没有导入。auto 和 eauto 依赖 Hint 数据库,如果相关引理的 Hint 注册在未导入的模块里,auto 会默默失败。
修复方法
诊断顺序:
- 确认 tactic 名称拼写正确;
- 用
Locate tactic_name.查找该 tactic 来自哪个模块; - 在文件顶部加入对应的
Require Import; - 对于
auto失败,用info_auto或debug auto打印搜索轨迹,确认 Hint 数据库内容。
1 | |
对 Hint 数据库,Print HintDb core. 可以列出 core 数据库中的所有引理,用于排查 auto 为何找不到某个引理:
1 | |
诊断工作流总结
面对卡住的证明,按以下顺序排查通常能定位根因。
第一步,Set Printing All 展开目标,排除记号和隐式参数的干扰,确认目标的真实形态。
第二步,Show Proof. 打印当前 proof term,检查是否有 evar 或意外的 admit 留在骨架里。
1 | |
第三步,对类型错误,Check @term 显示完整类型签名,About ident 显示透明度和来源模块。
第四步,对宇宙错误,加 Set Printing Universes 后重新触发错误,读取层级约束冲突点。
第五步,证明完成后,Print Assumptions 检查是否意外依赖了公理或 admit:
1 | |
参考资料
- Coq 官方文档:Tactics 章节 https://coq.inria.fr/doc/V8.19.0/refman/proof-engine/tactics.html
- Coq 官方文档:Universe Polymorphism https://coq.inria.fr/doc/V8.19.0/refman/addendum/universe-polymorphism.html
- 本系列第 4 篇:基础 tactic 全景(
induction、revert、generalize的用法对比) - 本系列第 5 篇:搜索与自动化(
auto、eauto、Hint 数据库机制) - 本系列第 7 篇:Ltac 编程(
info_auto、debug模式)
练习
练习 1:下面的证明会因为泛化不足而失败,修改它使其通过:
1 | |
提示:检查 induction l 之前是否需要 revert 某个变量。如果不需要,找到 rev_length 在标准库中对应的完整证明,对比归纳假设的形式。
练习 2:下面的代码触发宇宙不一致,找出原因并修复:
1 | |
提示:用 Set Printing Universes 观察层级标注,再考虑 Universe Polymorphism 是否适用。
练习 3:定义一个以 Defined 结尾的函数,验证它可以被 unfold 展开,而同名的以 Qed 结尾的版本不能:
1 | |
