Header

译自 Simplistic GADTs in OCaml。除非特殊声明,下文中「我」代指原文作者 Oleg Kiselyov
From oleg at http://okmij.org Fri Jul 10 20:05:10 2009
To: caml-list@inria.fr
Subject: GADTs in OCaml
Message-Id: 20090711030510.49092176DE@Adric.metnet.navy.mil
Date: Fri, 10 Jul 20091 20:05:10 -0700 (PDT)
Status: RO

本文展示了一种 OCaml 中的简单、无副作用、且不需要魔法的实现 GADTs23 的方法。这种实现足以覆盖 GADTs 的许多常见应用:表达带有不变式(invariant)的数据结构、有类型的 printf/scanf、无标签(tagless)解释器等。本文提出的具体实现只是一个简单的 ML 模块,不需要修改 OCaml 系统本身。由于实现十分简单,理论上可以在任意 ML 系统上运行(不过,就如同嵌套数据类型一样,在不支持多态递归(polymorphic recursion)的 SML 上,GADT 也不是很有用)。

本文的例子涵盖了:

  • 保障数据结构的不变式:静态地保证在一个表示某 HTML 文档的树结构中,一个链接节点不能为另一个链接节点的父节点。

  • 有类型的 printf/scanf 实现,两个实现之间共享相同的格式化描述符。

  • 带有常量和高阶抽象语法(High-Order Abstract Syntax)的简单有类型 $λ$-演算。我们所展示的其实就是奚宏伟等人(Xi et. al)的 POPL 2003 论文4中所展示的例子。

请移步 http://okmij.org/ftp/ML/GADT.ml 以获取带有实现和例子的完整代码。

GADT 本身的记法符号很轻量。然而 GADTs 通常是归纳数据类型,处理归纳数据类型又需要多态递归函数,因此又引入了一些(很小的)符号开销。最主要的是,GADTs 常与存在量化类型(existentially quantified type)并辔。在 Haskell 中,GADT 类型声明隐含了存在量化类型声明。遗憾的是,OCaml 却缺乏这样的符号便利,况且在 OCaml 中编码存在量化类型(通过双重否定)5并不美观。至于智能构造函数(smart constructor)——靠手搓,或许可以写一个合适的 camlp4 宏来减轻痛苦。

我们从探索的过程中学到的一课是:目前的 OCaml 就已经可以实现常用的 GADTs。在不对目前 OCaml 类型系统进行修改的前提下,我们就已经可以写出之前所提出的,使我们认为需要 GADTs 特性的例子——并且不会有太多记法符号上的麻烦。我们也可以或多或少地将 Haskell 中使用了 GADTs 的代码机械地转译为 OCaml 代码。当然,为 OCaml 的类型系统添加诸如显式存在量化类型,对 Rank-2 类型更好的支持6等特性无疑是锦上添花,但并不是使用和享受 GADTs 的必要条件。

希望本文提供的实现能让各位体验一下 GADTs,或帮助各位编写似乎依赖于 GADTs 特性的代码。GADT.ml 文件的结尾还提出了一种更高效的实现方法,这种方法或许可以作为 OCaml 中原生支持 GADTs 实现的基础。

正如 Patricia Johann 与 Neil Ghani 的《Foundations for Structured Programming with GADT》(POPL 2008)7所指出的。GADTs 的本质是 EQ GADT,即实现了以下接口的类型:

type ('a, 'b) eq
val refl : ('a, 'a) eq
val apply_eq : ('a, 'b) eq -> 'a -> 'b

类型 ('a, 'b) eq8 的值代表对「两个类型相等」这一命题的证明。而 apply_eq 利用了这一证明以执行安全的类型强制(coercion)

更准确地说:真正的 GADTs 实现对于任意类型 τ 都提供了一个表示莱布尼茨律(Leibniz’s principle)9的函数。该函数有以下类型签名

val apply_eq : ('a, 'b) eq -> 'a τ -> 'b τ

虽然我们的实现只能支持那些 Functor 式(即支持 map 操作)的类型 τ。不过,对于常见案例来说似乎已经足够了。

这一朴素实现也从原理上避免了出现段错误,因此是真正安全的。

module EQ = struct
  type ('a,'b) eq = Refl of 'a option ref * 'b option ref
  let refl () = let r = ref None in Refl (r,r)
  let symm : ('a,'b) eq -> ('b,'a) eq = function 
      Refl (x,y) -> Refl (y,x)
  let apply_eq : ('a,'b) eq -> 'a -> 'b = function
      Refl (rx,ry) -> fun x ->
        rx := Some x;
        match !ry with
        | Some y -> rx := None; y
        |     _  -> failwith "Impossible"
end;;

下面简要展示 GADT.ml 中的一个例子:有类型的 printf/scanf 实现,printf 与 scanf 之间互相共享格式化描述符:

let tp2 = sprintf (f_lit "Hello " ^^ f_lit "world" ^^ f_char) '!';;
(* val tp2 : string = "Hello world!" *)
let ts2 = sscanf tp2 (f_lit "Hello " ^^ f_lit "world" ^^ f_char) (fun x -> x);;
(* val ts2 : char * string = ('!', "") *)
(* 格式化描述符是头等对象,可以增量式创建 *)
let fmt31 () = f_lit "The value of " ^^ f_char ^^ f_lit " is ";;
(* val fmt31 : unit -> ('a, char -> 'a) fmt *)
let fmt3 () = fmt31 () ^^ f_int;;
(* val fmt3 : unit -> ('a, char -> int -> 'a) fmt *)
let tp3 = sprintf (fmt3 ()) 'x' 3;;
(* val tp3 : string = "The value of x is 3" *)
(* 我们不但可以打印值,也可以从字符串中读回值 *)
let ts3 = sscanf tp3 (fmt3 ()) (fun x n -> (x,n));;
(* val ts3 : (char * int) * string = (('x', 3), "") *)

这一例子是对下列 Haskell 代码的 OCaml 直接再实现

我们通过 GADT 定义了描述格式的领域特定语言(Domain Specific Language, DSL),而这一 GADT 正是用 OCaml 实现的,具体如下:

type ('a,'b) fmt =
  | FLit of < m_flit : 'w. (('a,'b) eq -> string -> 'w) -> 'w >
  | FInt of < m_fint : 'w. ((int -> 'a,'b) eq -> 'w) -> 'w >
  | FChr of < m_fchr : 'w. ((char -> 'a,'b) eq -> 'w) -> 'w >
  | FCmp of < m_fcmp : 'w. ('a,'b,'w) fcmp_k -> 'w >
      (* 存在量化类型的标准编码方式 *)
and ('a,'c,'w) fcmp_k = 
    {fcmp_k : 'b. ('b,'c) fmt * ('a,'b) fmt -> 'w}
;;

并(手动地)定义智能构造函数如下:

let f_lit x 
    = FLit (object method m_flit : 'w. (('a,'b) eq -> string -> 'w) -> 'w
        = fun k -> k (refl ()) x end);;
(* val f_lit : string -> ('a, 'a) fmt *)
let f_int 
    = FInt (object method m_fint : 'w. ((int -> 'a,'b) eq -> 'w) -> 'w
        = fun k -> k (refl ()) end);;
(* val f_int : ('a, int -> 'a) fmt *)

下面则展示了上述 DSL 的一个解释器,该解释器把值格式化为字符串:

type print_sig = {pr: 'a 'b. ('a,'b) fmt -> (string -> 'a) -> 'b};;
let rec printer = {pr = 
 function 
   | FLit x -> fun k -> x#m_flit (fun eq x -> apply_eq eq (k x))
   | FInt x -> fun k -> x#m_fint (fun eq   -> apply_eq eq 
         (fun x -> k (string_of_int x)))
   | FChr x -> fun k -> x#m_fchr (fun eq   -> apply_eq eq 
         (fun x -> k (String.make 1 x)))
   | FCmp x -> fun k ->
       x#m_fcmp {fcmp_k = 
          fun (a,b) -> printer.pr a (fun sa ->
                        printer.pr b (fun sb -> k (sa ^ sb)))}
};;
let sprintf fmt = printer.pr fmt (fun x -> x);;
(* val sprintf : (string, 'a) fmt -> 'a *)

代码非常简单,读者可以创建许多类似的解释器:例如,在不修改格式描述符或库本身的情况下,将格式化输出导向任意适合的数据出口。

参考