深入 Coq 05:搜索与自动化
Coq 的自动化 tactic 不是万能的黑盒,而是针对特定目标形状设计的搜索算法。auto 和 eauto 在 hint 数据库上做反向链推导;lia 在线性整数/自然数算术上调用决策程序;ring 和 field 把多项式等式规约到规范形式再比较。了解每个 tactic 的适用边界,才能在恰当的地方放手让机器搜,而不是在它注定失败的地方反复重试。前置阅读:《深入 Coq 02:目标窗口与 tactic 交互模型》和《深入 Coq 03:Gallina 核心语法速查》。
auto 与 hint 数据库
auto 的核心机制是反向链推导(backward chaining):从当前 goal 出发,匹配 hint 数据库中的引理头部,将 goal 替换为引理的前提,递归直到所有 subgoal 都被 assumption 或 reflexivity 关闭,或达到搜索深度上限(默认 5)。
1 | |
目标窗口在 auto 前:
1 | |
auto 成功,因为本地假设就是 hint(局部 intro 后的假设自动进入搜索空间)。
Hint 数据库机制
Coq 维护若干命名数据库,auto 默认使用 core 库。向数据库添加条目的命令:
1 | |
调用时指定数据库:
1 | |
Hint Resolve 把引理的结论与 goal 匹配;Hint Constructors T 等价于对 T 的每个构造子都做 Hint Resolve;Hint Unfold f 在尝试匹配前先展开 f。
深度参数
默认深度 5 常常不够。两种调整方式:
1 | |
深度越大,搜索时间指数增长。超过 20 时应改用 eauto 的 iterative deepening 或手动引导。
失败情形
auto 不处理等式变换、算术、量词实例化。以下 goal 对 auto 是死局:
1 | |
eauto 与存在变量
eauto 是 auto 的扩展版本,允许在搜索过程中创建存在元变量(evar),在后续 subgoal 中延迟实例化。这使它能处理 auto 无法处理的存在量词和参数尚未确定的目标。
1 | |
auto 在同一目标上失败,因为它不会引入 evar。
更实际的例子:利用传递性引理构造链式证明:
1 | |
Nat.le_trans 的类型是 forall n m p, n <= m -> m <= p -> n <= p,中间项 m 和 p 在链式应用时需要被推断,eauto 通过 evar 延迟绑定完成。
eauto 的代价
eauto 搜索空间比 auto 大得多,相同深度下运行时间可能差一个量级。高度依赖 evar 的搜索还会产生难以调试的失败信息。实践原则:先用 auto,失败且目标含存在量词或需要延迟实例化时再换 eauto。
omega(已废弃)与 lia
omega 的历史地位
omega 曾是 Coq 的标准线性整数算术决策程序,处理含加法、减法、常量乘法的整数/自然数等式和不等式。在 Coq 8.14 中被标记为废弃,8.19 中仍可用但会产生警告:
1 | |
遗留代码库和教材中仍大量出现 omega。遇到时直接替换为 lia,行为基本兼容。
lia 的能力范围
lia(Linear Integer Arithmetic)是 omega 的替代品,底层实现了 Simplex 的整数扩展。能处理:
1 | |
lia 无法处理的情形
lia 限于线性算术。乘法、除法(除以常数以外)、模运算、指数均超出范围:
1 | |
这里的修复路径:lia 在归纳步 n * n >= n -> (S n) * (S n) >= S n 上成功,因为展开后得到 n*n + 2*n + 1 >= n + 1,n*n 此时作为已知假设可当作一个整体处理。
omega 能做而 lia 不能的情形
lia 在绝大多数场景优于 omega。唯一已知的退步是极少数涉及自然数减法下界推理的特殊模式,但在 Coq 8.16+ 中已修复。实际使用中两者对同一 goal 的行为一致。
ring 与多项式等式
ring 证明交换环(commutative ring)上的多项式等式,包括 nat、Z、Q、R 以及用户定义的环结构。它把两边都规约到同一规范形式(按字典序排列的单项式之和),再做语法比较。
1 | |
ring 的成功前提是目标是纯等式且两边都是多项式表达式,不包含假设中的等式。带条件的等式需要先用 rewrite 替换,再交给 ring:
1 | |
ring_simplify 与 ring_iff
ring_simplify 化简目标中的多项式子表达式,而不要求两边完全相等。ring 也可通过 ring_iff 用于等价命题(<->),但语法稍有不同,通常不如直接 split; intro; ring 简洁。
field 与有理表达式
field 是 ring 在域(field)上的扩展,处理带除法的等式。它在内部调用 ring,并附加非零条件的侧目标。
1 | |
field 完成后往往留下若干 <> 0 形式的 subgoal,需要手动提供非零证明。这是它与 ring 最显著的差异——ring 对无除法的表达式无副作用,field 必须处理除数。
失败案例与修复对照:
1 | |
自定义 hint 数据库:完整示例
下面构造一个小型例子,展示从定义到证明的完整流程,包含自定义 hint 数据库、auto 的成功与失败、以及 Show Proof. 的输出。
1 | |
Show Proof. 在证明完成后输出:
1 | |
这证明了 auto with all_db 在归纳步骤中确实在使用 All_cons,而不是不透明的 magic。
失败示例与修复路径
尝试一:直接用 auto 证明排列引理
1 | |
auto with * 失败是因为 Permutation 的证明需要多个构造子连续组合,单步匹配无法收敛。
修复:手动展开后交给 auto
1 | |
对于 Permutation 这类组合深度大的谓词,手动指定中间步骤、最后几步交给 auto 是比依赖全自动搜索更稳健的做法。
Print Assumptions 的用途
证明完成后 Print Assumptions 列出证明树中用到的所有公理,用于确认没有引入意外的经典逻辑或排中律:
1 | |
输出:
1 | |
这表明 lia 的证明完全在 CIC(构造性演算)框架内,无需额外公理。若某个证明引入了 Classical.classic 或 FunctionalExtensionality.functional_extensionality,Print Assumptions 会如实列出,方便判断是否符合当前项目对构造性的要求。
各 tactic 选用判断
| 目标形状 | 优先尝试 | 备注 |
|---|---|---|
| 命题逻辑,局部假设充足 | auto |
不够就加深度或 tauto |
| 存在量词,需延迟实例化 | eauto |
深度大时运行慢 |
| 线性整数/自然数算术 | lia |
非线性先归纳再 lia |
| 交换环多项式等式 | ring |
含 rewrite 时先替换再 ring |
| 域(含除法)等式 | field |
注意非零侧目标 |
| 无规律的命题 | 手动 + 局部 auto |
自动化做收尾,不做主干 |
练习
练习 1:证明下列引理,要求在最终步骤使用 lia,不使用 omega:
1 | |
提示:Nat.div_mod 说明 n = n / 2 * 2 + n mod 2,Nat.mod_upper_bound 给出 n mod 2 < 2。先用 rewrite (Nat.div_mod n 2) 展开,再用 lia(结合 Nat.mod_upper_bound n 2 的结论)关闭。
练习 2:创建一个 hint 库 even_db,加入以下两条引理,再用 auto with even_db 证明 Even 4:
1 | |
练习 3:用 ring 证明:
1 | |
如果 ring 直接失败(某些版本对 ^3 展开策略不同),尝试先 ring_simplify,再 ring,或手动展开 ^3 后再调用。
参考资料
- Coq 8.19 参考手册,Tactics chapter,§ auto, § eauto, § lia, § ring, § field
- Adam Chlipala, Certified Programming with Dependent Types,Proof Automation 章节
- 《深入 Coq 01:开发环境与项目结构》
- 《深入 Coq 02:目标窗口与 tactic 交互模型》
- 《深入 Coq 03:Gallina 核心语法速查》
