深入 Coq 12:类型类与 Canonical Structures
Coq 提供两套独立的"重载"机制:类型类(Type Classes)和典范结构(Canonical Structures)。两者表面都是让用户向一个通用接口注册实现,但底层驱动方式截然不同:类型类依赖 unification 变量加实例搜索,典范结构依赖投影展开后的合一。MathComp 库选择典范结构作为代数层次的支柱,这一设计决策影响了整套库的证明风格。本篇覆盖两套机制的语义、调试手段、权衡比较,以及 MathComp 如何用典范结构把 eqType、choiceType、zmodType、ringType、fieldType 串成一条可继承的层次链。前置阅读:《深入 Coq 03:Gallina 核心语法速查》和《深入 Coq 07:Ltac 编程》;代数基础见《依值类型——从命题逻辑到一阶逻辑》。
类型类的核心机制
Class 与 Instance 声明
Coq 的 Class 关键字声明一个带有命名字段的 record,附带一个隐式实例参数占位符。Instance 声明将某个具体类型注册为该 class 的实现。
1 | |
声明完成后,任何接受 {e : Eq A} 隐式参数的函数都可以直接用 eqb,Coq 的类型类搜索引擎(TC elaborator)会自动填充 EqNat。
1 | |
Existing Instance 与手工注册
当某个引理或定义的返回值已经是一个类的实例,但希望让搜索引擎能找到它时,使用 Existing Instance:
1 | |
此后,遇到 Eq (nat * bool) 的搜索请求,引擎会尝试 pair_eq 并递归填充 EqNat 和 EqBool。
实例搜索顺序与优先级
Coq 的类型类搜索按**优先级(priority)**从高到低尝试所有注册的实例,默认优先级为 100,数字越小优先级越高。可以在声明时显式指定:
1 | |
搜索过程是深度优先回溯:若某个实例的子目标无法满足,引擎回退并尝试优先级次低的候选。搜索深度由 Typeclasses Depth 控制,超过上限时报错而非无限循环。
调试类型类搜索
当实例搜索失败或产生意外结果时,Set Typeclasses Debug 输出完整的搜索轨迹:
1 | |
输出样例(简化):
1 | |
若出现 [TC] backtracking,说明某个候选实例的子目标无法满足,引擎正在回退。
典范结构的核心机制
Structure 与 Canonical 声明
典范结构(Canonical Structures)的核心是 Coq 的 record 系统加上合一提示(unification hint)。Canonical 声明告诉合一引擎:当试图把某个投影和某个值合一时,优先展开到指定的 record 实例。
1 | |
声明 Canonical nat_EqStruct 之后,当合一引擎遇到形如 eq_op ?S x y(?S : EqStruct),且 x y : nat 已知时,会自动将 ?S 合一为 nat_EqStruct。
合一驱动的自动填充
典范结构的填充不经过专门的搜索引擎,而是由 Coq 内核的合一算法触发。填充发生在类型检查阶段而非精化阶段,调试时使用 Print Canonical Projections,失败时报合一错误而非"实例未找到"。
1 | |
失败示例与修正
一个常见错误:忘记把类型参数通过 :> 声明为强制转换,导致合一引擎找不到载体。
1 | |
正确写法是把 carrier 改为 carrier :> Type,让 Coq 知道 BadStruct 可以直接当作类型使用,合一引擎才能在看到具体类型(如 nat)时查找典范实例。
1 | |
类型类与典范结构的权衡
搜索机制的差异
类型类使用专用的实例搜索引擎,可以处理复杂的递归依赖(如 Eq (A * B) 需要 Eq A 和 Eq B),但搜索过程对用户不透明,失败报错有时难以定位。
典范结构依赖合一,触发条件更精确——只有当某个投影的具体值可从上下文中确定时才触发。这使得典范结构更可预测,但也更不灵活:无法表达"如果 A 有实例且 B 有实例,则 A × B 也有实例"这类递归规则(除非手工展开)。
层次结构的表达能力
| 维度 | 类型类 | 典范结构 |
|---|---|---|
| 触发机制 | 隐式参数 + 实例搜索 | 投影展开 + 合一 |
| 递归实例 | 支持(通过搜索递归) | 需手工或 Canonical 链 |
| 优先级控制 | | n 语法 |
无内置优先级,按声明顺序 |
| 层次继承 | Extends 或字段复用 |
Structure 字段嵌套 |
| 主要用途 | 通用重载、Haskell 风格接口 | 数学代数层次(MathComp 风格) |
| 调试命令 | Set Typeclasses Debug |
Print Canonical Projections |
我倾向于在需要表达类 Haskell 的"接口+实现"时用类型类,在需要像 MathComp 那样构建严格的代数层次时用典范结构。两者也可混用,但要清楚哪个机制在何处接管。
MathComp 的代数层次
层次结构概览
MathComp 用典范结构把代数层次组织成以下链条(从弱到强):
1 | |
每个层次都是一个 Structure,其 carrier 字段携带底层类型,上层结构的字段包含下层结构的实例(作为"混入")。
eqType 的典范注册
1 | |
ringType 的结构字段链
1 | |
x y : R 在 R : ringType 的上下文里,+、*、- 都解析为 ringType 的对应运算,因为 ringType 的 sort 字段通过 :> 声明为强制转换,合一引擎能自动把 R 当作类型使用。
从 nat 到 int 的层次继承
1 | |
sqr_ge0 的类型签名只要求 numDomainType,而 int 通过典范结构链被自动认定为该层次的实例。针对抽象代数结构证明的引理,对所有满足条件的具体类型直接可用,无需重新证明。
手工构建一个 eqType 实例
下面演示如何为自定义类型注册到 MathComp 的 eqType 层次:
1 | |
EqMixin 和 EqType 是 MathComp 提供的构造函数,Canonical color_eqType 告诉合一引擎:当遇到 Color 需要 eqType 实例时,使用 color_eqType。
调试与诊断
Print Canonical Projections 的读法
Print Canonical Projections 输出所有已注册的典范映射,每行格式为:
1 | |
例如:
1 | |
表示:当合一引擎遇到 Equality.sort ?e 且需要将其与 nat 合一时,自动选择 nat_eqType。若某个类型在某个投影下没有出现,说明它还未注册到对应的结构层次。
Set Typeclasses Debug 的读法
对于类型类,Set Typeclasses Debug Verbosity 2 输出更详细的搜索过程:
1 | |
输出中 Resolve 表示尝试一个实例,Success 表示匹配成功,Fail 表示回退。层级缩进反映搜索树的深度。
常见错误模式
Cannot unify ... with ...:典范结构未注册,或载体字段缺少:>。Unable to satisfy the following constraints:类型类搜索失败,可用Set Typeclasses Debug定位缺失的实例。Ambiguous instance:同一类有两个同优先级的实例都能匹配,结果不确定,应显式指定或调整优先级。
与其他语言的对比
Coq 类型类在语法上受 Haskell 影响,但语义差异明显:Haskell 的类型类在编译时完全确定,Coq 的类型类搜索发生在精化阶段,可以依赖运行时未知的类型变量。
典范结构在语义上更接近 C++ 的模板特化(template specialization):通过具体类型触发特定实现,但 Coq 的版本完全基于合一而非模式匹配,更接近类型论的核心。
Lean 4 把两者统一到一套 class / instance 机制下,通过 inferInstance 和 synthesizeInstance 明确区分。Agda 使用 record 加 instance 参数,不区分两种机制。
参考资料
- Sozeau, M., & Oury, N. (2008). First-Class Type Classes. TPHOLs 2008.
- Mahboubi, A., & Tassi, E. (2013). Canonical Structures for the Working Coq User. ITP 2013.
- MathComp 官方文档:https://math-comp.github.io/
- Coq Reference Manual §20 (Type Classes):https://coq.inria.fr/doc/
- 本系列《深入 Coq 03:Gallina 核心语法速查》
- 本系列《深入 Coq 07:Ltac 编程》
练习 1:为二叉树注册 eqType
定义一个 BTree A 类型(叶节点和内部节点),在 A : eqType 的前提下,实现 btree_eqb,证明 Equality.axiom,并注册 Canonical btree_eqType。验证 (Leaf : BTree nat) == Leaf 能通过类型检查。
练习 2:自定义类型类与实例优先级
声明一个 Printable A 类型类,字段为 to_string : A -> string。分别为 nat、bool、list nat 注册实例。然后为 nat 注册一个使用十六进制的替代实例,优先级设为 0(最高)。验证 to_string 255 输出十六进制结果,而 to_string [1; 2] 仍使用默认 nat 的十进制格式。
练习 3:手工构建 zmodType 实例
定义 ZMod2(二元域 GF(2)),实现加法(异或)和零元,证明满足 zmodType 所需的公理(交换律、结合律、零元、逆元),并注册为 MathComp 的 zmodType 典范实例。验证 (1 : ZMod2) + 1 = 0 可以用 ring 或 by [] 完成。
