使用具有更高阶函数的GADT

时间:2017-10-21 17:29:42

标签: types ocaml higher-order-functions gadt locally-abstract-type

我试图模拟异构树",即。节点有不同种类的树#34;和每个" kind"被限制在"种类"他们可能包含的儿童:

type id = string
type block
type inline

type _ node =
  | Paragraph : id * inline node list -> block node
  | Strong : id * inline node list -> inline node
  | Text : id * string -> inline node

然后可以像这样定义树:

let document =
    Paragraph ("p1", [
      Text ("text1", "Hello ");
      Strong ("strong1", [
        Text ("text2", "Glorious")
      ]);
      Text ("text3", " World!")
  ])

通常这可以使用单独的变体为每种"种类"节点,但我试图将其定义为GADT,以便能够使用在每种节点上模式匹配的高阶函数来操作树:

function
  | Text ("text2", _) ->
    Some (Text ("text2", "Dreadful"))
  | _ ->
    None

我遇到的问题是定义接受上述高阶函数的函数并将其应用于每个节点。到目前为止,我有这个:

let rec replaceNode (type a) (f: a node -> a node option) (node: a node): a node =
  match f node with
  | Some otherNode -> otherNode
  | None ->
    match node with
    | Paragraph (id, children) -> 
      Paragraph (id, (List.map (replaceNode f) children))
    | Strong (id, children) ->
      Strong (id, (List.map (replaceNode f) children))
    | Text (_, _) -> node

但编译器在突出显示的行上给出了以下错误

  

此表达式具有类型块节点 - >一个节点选项,但是一个表达式需要类型块节点 - >节点选项这个块的实例是不明确的:它将逃避其等式的范围

或者,如果我将f的类型更改为'a node -> 'a node option,我会收到此错误

  

此表达式具有类型一个节点但是表达式需要类型一个节点类型构造函数a将转义其范围

显然,我并不完全理解本地抽象类型(或GADT真的,就此而言),但从我的理解中看出这些错误似乎是因为类型是,顾名思义,"本地",而在外部多态,传递它会"泄漏"它,我猜?

所以我的问题,首先是:这是否可行(并且通过"这"我认为我的意思是在GADT上进行模式匹配更高阶的功能,但我甚至不确定是否存在实际问题?

Playground with all the code here

1 个答案:

答案 0 :(得分:6)

这里存在两个根本问题(由于GADT的存在而有点混乱)。第一个问题是replaceNode是二级多态函数。实际上,在第一次匹配中,f应用于a node类型的节点,但在Paragraph分支内,它将应用于inline node类型的节点。这里的类型检查错误有点复杂List.map调用,但是将函数重写为

let rec replaceNode (type a) (f:a node -> a node option) 
(node:a node): a node =
  match f node with
  | Some otherNode -> otherNode
  | None ->
    match node with
    | Paragraph(id, []) -> Paragraph(id,[])
    | Paragraph (id, a :: children) -> 
      Paragraph (id, f a :: (List.map (replaceNode f) children))
    | Strong (id, children) ->
      Strong (id, (List.map (replaceNode f) children))
    | Text (_, _) -> node;;

产生更直接的错误:

  

错误:此表达式具有类型内联节点
  但是预期表达式为节点
  类型内联与类型<

不兼容

问题是我们需要确保f适用于任何类型a的类型检查器,而不仅仅是原始类型a。换句话说,f的类型应为'a. 'a node -> 'a node option(又名forall 'a. 'a -> 'a node option)。不幸的是,显式多态注释只能在OCaml的第一个位置(prenex)中使用,因此我们不能只改变freplaceNode的类型。但是,可以通过使用多态记录字段或方法解决此问题。

例如,使用记录路径,我们可以定义记录类型mapper

type mapper = { f:'a. 'a node -> 'a node option } [@@unboxed]

其中字段f具有正确的显式多态符号(也称为通用量化),然后在replaceNode中使用它:

let rec replaceNode (type a) {f} (node: a node): a node =
  match f node with
  | Some otherNode -> otherNode
  | None ->
    match node with
    | Paragraph (id, children) -> 
      Paragraph (id, (List.map (replaceNode {f}) children))
    | Strong (id, children) ->
      Strong (id, (List.map (replaceNode {f}) children))
    | Text (_, _) -> node

然后弹出第二个问题:这个replaceNode函数有类型 mapper -> inline node -> inline node。内联类型来自哪里?这个问题的时间是多重递归。如果没有明确的多态注释,replaceNode的类型在其递归定义中被认为是常量。换句话说,类型检查器认为replaceNode对于给定的 mapper -> 'elt node -> 'elt node类型'elt具有paragraph。在strongchildren分支中,inline node列表是List.map (replaceNode {f}) children的列表。因此,'elt意味着类型检查器inline = replaceNode,因此mapper -> inline node -> inline node的类型变为let rec replaceNode: type a. mapper -> a node -> a node = fun {f} node -> match f node with | Some otherNode -> otherNode | None -> match node with | Paragraph (id, children) -> Paragraph (id, (List.map (replaceNode {f}) children)) | Strong (id, children) -> Strong (id, (List.map (replaceNode {f}) children)) | Text (_, _) -> node;;

要解决此问题,我们需要添加另一个多态注释。幸运的是,这一次,我们可以直接添加它:

mapper -> 'a node -> 'a node

最后,我们获得了let f: type a.…类型的函数。 请注意,(type a)是用于组合本地抽象类型和显式多态注释的快捷方式。

完成解释时,这里需要局部抽象a因为只有抽象类型可以在GADT上进行模式匹配时进行细化。换句话说,我们需要准确地确定ParagraphStrongText中的a类型遵循不同的等式:block = {{1}在段落分支中,ainline分支Strong = Text

编辑:如何定义映射器?

这个局部抽象类型位在定义映射器时实际上很重要。 例如,定义

let f = function
  | Text ("text2", _) -> Some (Text ("text2", "Dreadful"))
  | _ -> None

inline node -> inline node option生成f类型,因为通过构造函数Text进行匹配会产生相等'type_of_scrutinee=inline

要纠正这一点,需要添加局部抽象类型注释 使类型检查器能够逐个细分每个细分的类型:

 let f (type a) (node:a) : a node option= match node with
 | Text ("text2", _) -> Some (Text ("text2", "Dreadful"))
 | _ -> None

然后这个f有正确的类型,可以包装在映射器记录中:

let f = { f }

广告:所有这些都在OCaml手册中详细说明,从版本4.06开始。