变体与 GADT 方法

Variant vs GADT approach

在 OCaml 中,我想定义一个函数 f 接受 input 来更新记录 x。在以下两种方法中,我感兴趣的是一种是否比另一种有优势(除了可读性)。

变体方法

type input =
  | A of int
  | B of string

let f x = function
  | A a -> { x with a }
  | B b -> { x with b }

GADT 方法

type _ input =
  | A : int input
  | B : string input

let f (type t) x (i: t input) (v: t) =
  match i with
  | A -> { x with a = v }
  | B -> { x with b = v }

ADT 专家:

  • 简单明了,不需要类型注释或任何花哨的东西
  • 编写 string -> input 类型的函数很简单。

GADT 专业人士:

  • 免一层拳。
    但是,如果你需要一个解析函数,这将完全否定,这将迫使你在存在下打包东西。

更准确的说,GADT版本可以看作是ADT版本的分解。你可以系统地把一个转换成另一个,内存布局也会类似(借助一个小注解):

type a and b and c

type sum =
  | A of a
  | B of b
  | C of c

type _ tag =
  | A : a tag
  | B : b tag
  | C : c tag

type deppair = Pair : ('a tag * 'a) -> deppair [@@ocaml.unboxed]

let pack (type x) (tag : x tag) (x : x) = Pair (tag, x)
let to_sum (Pair (tag, v)) : sum = match tag with
  | A -> A v
  | B -> B v
  | C -> C v

let of_sum : sum -> deppair = function
  | A x -> pack A x
  | B x -> pack B x
  | C x -> pack C x

正如您所注意到的,GADT 的(非)可读性是一个很大的缺点。尽可能避免使用 GADT。更容易打字,更容易阅读。错误消息也不太复杂。

在运行时简化它们是相同的。它们表示为带有标签和字段的简单整数或块,并且代码使用标签来匹配和分支。所以两者都不给你优势。

在编译时,GADT 更加强大,因为编译器可以以 ADT 不允许的方式检查类型。一个例子是存在类型,就像另一个答案中的例子一样。所以不能用ADT的时候就用GADT。