Skip to content

定义Definition

广义代数数据类型 GADT

Generalized algebraic data type · GADT

通过构造器的结果类型取得分支局部等式,使ML式索引数据的消去结果随类型参数精确变化。

形式陈述 ​

广义代数数据类型GADT允许同一类型构造器的不同构造器返回不同参数实例。普通代数数据类型的构造器通常统一返回T a;GADT可以让某个构造器只返回T Int,另一个只返回T Bool。匹配构造器时,这些结果类型提供分支局部的类型等式。

固定带Int、Bool、函数和模式匹配的ML式核心,定义

text
data Expr a where
  Lit   : Int → Expr Int
  Truth : Bool → Expr Bool
  Add   : Expr Int → Expr Int → Expr Int
  Less  : Expr Int → Expr Int → Expr Bool
  If    : ∀a. Expr Bool → Expr a → Expr a → Expr a

索引a是类型参数,不是运行时整数。构造Expr值时,按普通函数应用规则检查构造器参数;例如Add的两个子表达式都必须是Expr Int。

消去时,设被匹配对象已知为Expr α。某分支构造器的结果为Expr T,则在该分支假设 α≡T,并用这个等式检查局部变量及预先给定的结果类型。不能把分支等式当作永久替换,带到其他分支或函数的所有调用中。

本页采用明确注解的检查纪律:匹配对象的索引和目标结果类型来自已知签名,外层全称变量是刚性的;分支等式是模式提供的局部证据。合一可用于分解构造器类型等式,但不等于把所有刚性变量交给普通推断任意求解。实际系统也可用显式等式约束或类型强制表示这些证据。

直觉

一棵普通语法树告诉你节点叫“加法”或“小于”。GADT还让构造器向类型系统保证:加法节点的两个孩子能产生整数,小于节点会产生布尔值。

当求值器看见Lit,便同时知道当前表达式的结果类型是Int,所以返回其中的整数合理;看见Less,就知道结果类型是Bool,所以返回比较结果合理。模式不是在运行时猜测一个未知值的类型,而是在揭开构造时已经受检查的证据。

例子与边界

一个无需通用Value包装的求值器 ​

给出显式签名

text
eval : ∀a. Expr a → a
eval e =
  match e with
  | Lit n       -> n
  | Truth b     -> b
  | Add p q     -> eval p + eval q
  | Less p q    -> eval p < eval q
  | If c p q    -> if eval c then eval p else eval q

检查时先取任意刚性α,输入e:Expr α,目标结果为α。逐分支的账本为:

分支 局部等式 子表达式类型 分支结果
Lit n α≡Int n:Int Int,经等式视作α
Truth b α≡Bool b:Bool Bool,经等式视作α
Add p q α≡Int p,q:Expr Int 两次递归各得Int,相加得Int
Less p q α≡Bool p,q:Expr Int 两次递归各得Int,比较得Bool
If c p q 构造器参数β与α相等 c:Expr Bool,p,q:Expr α 条件得Bool,两支各得α

eval在Less分支中以Int实例递归,而当前输入结果索引是Bool;在If中又以Bool和α分别递归。因此完整全称递归签名也承担多态递归的检查责任。

从构造到结果的一次完整运行 ​

取

text
e = If (Less (Lit 2) (Lit 5))
       (Add (Lit 7) (Lit 4))
       (Lit 0)

Less子树类型是Expr Bool。Add子树和Lit 0都为Expr Int,故If实例化为Int,整个e:Expr Int。运行先比较2<5得到true,再计算7+4得到11,不执行else分支;eval e:Int,结果11。

若把else改成 Truth false,If要求两支同为Expr a,却得到Expr Int和Expr Bool,构造e时就失败。无需等到eval运行后再把一个布尔结果错误地当整数使用。

如果改写求值器,让 Less p q -> 0,匹配会给出α≡Bool,目标因而是Bool,整数0不符合,分支检查失败。GADT不是允许不同分支随意返回不同类型;它要求每个返回值都满足该分支所学到的索引等式。

等式不能跑到分支外 ​

Lit分支知道α≡Int,不代表eval的全称α从此都是Int。检查Truth分支时需要重新从原环境开始,再加入α≡Bool;若把前一分支的替换全局保留,就会错误地产生Int=Bool冲突,或把整个eval不当地专门化。

一个只接收Expr Int的函数若列出Less分支,则构造器结果Expr Bool与输入Expr Int矛盾。对本页互异基础类型,能据此认定该分支不可达。完整GADT语言的覆盖检查可能更复杂;这里的简单矛盾判断不等于宣称所有索引可达性都容易决定。

推论与应用

如果去掉索引,改成单一 RawExpr,一个同时处理整数和布尔结果的普通求值器通常返回 Value = VInt Int | VBool Bool,并在加法处检查孩子是否真为VInt。GADT把这项结构正确性前移到构造和类型检查阶段,让eval的返回类型直接对应输入索引。

注解同样重要。按期待类型检查为匹配提供明确的Expr α和结果α;若检查器还在猜它们,什么时候可利用分支等式会影响推断顺序与可预测性。实际ML/Haskell系统因此采用特定的刚性注解、局部抽象类型或约束求解纪律,不能从这份已标注求值器推断“所有GADT程序都有HM式主类型”。

这里的索引等式与依赖模式匹配有共同思想,但本页没有让类型依赖任意项,也没有引入Vec长度motive或一般依赖消去子。它给出的具体新增能力是:在ML式类型参数上,通过构造器结果类型获得局部等式,并据此检查普通函数体。

本单元终点题解把这份Expr接口与可变缓存、多态参数和Eq字典放到同一小库中,逐项检查它们各自需要的证据,避免把不同扩展的规则混用。

参考资料
  • Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, Geoffrey Washburn, “Simple Unification-Based Type Inference for GADTs,” ICFP, 2006, 50–61;作者机构托管草稿,§§2–4:构造器结果类型、刚性注解与局部精化
  • Microsoft Research论文记录:正式发表信息及作者稿入口
关系图谱7 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系