OCaml中`!+'a t`类型中`!+`的含义是什么?

13

我目前正在学习OCaml,特别是functor。我查看了标准库中的map.mli,在大约第70行左右,有以下内容:

type key
(** The type of the map keys. *)

type !+'a t
(** The type of maps from type [key] to type ['a]. *)

val empty: 'a t
(** The empty map. *)

我知道key是映射中使用的键类型(或者说它的签名,因为我们在一个 .mli 文件中),'a t是映射本身的(多态/抽象)类型。但我想知道!+用于什么。我尝试查找相关文档,但不幸的是没有找到任何信息。
如果可能的话,请解释一下这个问题或提供相关文档/教程链接。
谢谢。

3
根据手册,每个类型参数可以用+(正)或-(负)前缀表示其是协变(正)还是逆变(负),并且可以使用感叹号!表示该参数可以从整个类型中推导出来。具体含义需要其他人进行详细解释 :) 与此同时,这些都是搜索更多信息的好关键词。 - glennsl
1
进一步说,注入性注释仅对抽象类型和私有行类型必要,因为它们可以从类型声明中推导出来:对于记录和变体类型声明(包括可扩展类型),所有参数都是注入性的;对于类型缩写,如果其定义方程中具有注入性出现,则参数是注入性的(无论是否为私有)。 - glennsl
1
对于类型缩写中的约束类型参数,如果它们出现在注入位置或所有类型变量都是可注入的,则它们是可注入的;特别地,如果约束类型参数包含一个未出现在主体中的变量,则它不能是可注入的。 - glennsl
1
如果您感兴趣,可以在此论文中了解更多细节(我并不是很感兴趣):https://ocaml.org/meetings/ocaml/2013/proposals/injectivity.pdf - glennsl
1个回答

13
方差和单射注释提供了有关抽象类型构造器 type 'a t 和其参数之间关系的一些信息。例如,类型构造器可能会:

  • 生成或包含一个 'a
type 'a t = 'a * 'a
  • 消费一个'a
type 'a t = 'a -> unit
  • 完全忽略它的参数:
type 'a t = int
  • 包含对 'a 的可视和可变引用
type 'a t = { get: unit -> 'a; store: 'a -> unit }

所有这些类型构造函数都可以通过签名进行抽象化,如下所示:
module Any: sig
  type 'a t
end = struct
   type 'a t = 'a * 'a
end 

通过这种抽象程度,我们对外部世界中的 'a t 一无所知。然而,有时了解多一点点信息会很有用。

例如,如果我有一个生产者类型构造函数'a t,例如:

type 'a t = 'a * 'a

有两种相关的类型,它们之间存在子类型关系,假设 type x = < x:int >type xy = <x:int; y:int > t。我可以从 xy <: x 推断出 xy t <: y t,因为假装存储的对象比实际生成的对象少一些方法是没问题的。由于关系 <: 的顺序从 xy :> xxy t :> x t 是保留的,我们说类型构造器 t 在其类型参数上是协变的。我可以通过添加一个变异注释来暴露类型构造器是协变的事实:

type xy = <x:int; y:int>
type x = < x:int >

module Covariant: sig
  type +'a t
  val xy: xy t
end = struct
  type +'a t = 'a * 'a
  let xy = let c = object method x = 0 method y = 1 end in c, c
end

let xy = (Covariant.xy: xy Covariant.t :> x Covariant.t)

如果我有一个消耗具有方法 x 的对象的类型,例如


type 'a t = 'a -> int
let x: x t = fun arg -> arg#x

假装需要更多的方法是可以的。换句话说,我可以将x t强制转换为xy t,从而将关系从xy:>x反转为x t :> xy t。我可以使用逆变注释来公开这些信息。

module Contravariant: sig
  type -'a t
  val x: x t
end = struct
  type -'a t = 'a -> int
  let x c = c#x
end

let xy = (Contravariant.x: x Contravariant.t :> xy Contravariant.t)

使用逆变的+-反映了正数乘以常规顺序x < y会保留,即2 x < 2 y,而负数乘以常规顺序则会翻转:x < y意味着-2 y < -2x
因此,方差注释允许我们暴露类型构造函数t相对于子类型的行为方式。 对于具体的类型定义,类型检查器将自行推断方差,无需进行任何操作。 但是,对于抽象类型构造函数,作者有责任公开(或不公开)方差信息。
在处理对象、多态变量或私有类型时,这种方差信息非常有用。但是,人们可能会想知道在OCaml中是否很重要,因为对象并没有被广泛使用。实际上,协变注释的主要用途依赖于多态性和值限制。
值限制是多态性和可变性之间破坏性干扰的结果。 最简单的例子是生成一对函数以从引用中存储或获取某个值的函数。
type 'a t = { get: unit -> 'a; store: 'a -> unit }
let init () =
  let st = ref None in
  let store x = st := Some x in
  let get () = match !st with None -> raise Not_found | Some x -> x
  in
  {store; get}

通常,我可以像这样使用:

let st = init ()
let () = st.store 0
let y = st.get ()

然而,在前面例子的第一行中,st的类型是什么?init的类型是unit -> 'a t,因为我可以在生成的引用中存储任何类型的值。然而,具体值st的类型不能是多态的,因为我不应该能够存储一个整数并检索一个函数:

let wrong = st.get () 0

因此,st 的类型是一种弱多态类型:'_weak1 t',其中'_weak1是一个占位符,表示只能替换一次的具体类型。因此,在第2行,我们可以了解到st的类型为'weak1=int',并且t的类型更新为int t。这就是值约束在发挥作用,它告诉我们不能普遍地推断出计算结果是多态的。然而,这个问题只是因为有了存储器,我们可以读取和写入类型为'a'的值。如果我们只能读取(或生成)类型'a'的值,则可以从计算中产生泛型值。例如,在这个例子中:
let gen_none () = None
let none = gen_None () 

令人惊讶的是,none 的推断类型是完全多态的类型 'a option'。事实上,选项类型 'a option 是一个不可变的容器类型,只能产生类型为 'a 的值。因此,将计算的类型从 '_weak1 option 泛化为 'a option 是可以的。这就是我们再次遇到的协变注释:作为一个只能产生 'a 的容器类型是描述协变类型参数的一种方式。并且可以更普遍地证明,如果一个类型变量只出现在协变位置,那么泛化该计算的类型总是可以的。这就是放松的值限制。

然而,这是因为对于协变类型构造函数 'a t',产生多态值的唯一可能方式是拥有某种空容器。通过 OCaml 类型系统检查这一点可能会很有趣:

type 'a maybe = Nothing | Just of 'a
type empty = |
type poly_maybe = { x: 'a. 'a maybe}
let a_polymorphic_maybe_is_nothing {x} = match (x:empty maybe) with
| Nothing -> ()
| _ -> .

总之,方差注释对于容器库很有用,可以使用户以零运行时成本的子类型强制整个容器,计算多态“空”值。
注入性的用例更加微妙。简而言之,在存在GADTs的情况下检查某些模式匹配的穷尽性时才会起作用(这就解释了为什么它们仅在OCaml 4.12中引入)。
实际上,如果方差注释涉及子类型关系的保留,那么注入性注释则涉及不等式的保留。想象一下我有两种可区分的类型,例如int和float。我是否总能区分int t或float t?如果我看看总和类型
type !'a sum = A

或记录
type !'a product = { x: 'a}

答案总是肯定的。换句话说,如果我有 'a != 'b ,那么 'a sum != 'b sum'a product !='b product 都成立。这种不等式的保持被称为单射性。我可以添加一个单射性注释来检查类型检查器是否同意我的想法(与方差注释一样,具体类型定义会推断出单射性注释)。
然而,如果类型参数实际上是幻影类型参数,则...
type 'a t = float

那么我不能保证 int t != float t (因为 _t 总是一个 float)。 举个带有幽灵参数的类型构造函数例子,我可以定义一个带单位的浮点数类型,如下:

type m = M
type s = S

module SI: sig
  type 'a t
  val m: m t
  val s: s t
end = struct
  type 'a t = float
  let m = 1.
  let s = 1.
end

在这里,类型'a SI.t表示具有编码为类型的单位的实数。 使用这个例子,我既有单射类型也有非单射类型。什么情况下需要注意单射性呢? 答案是,要使单射性成为问题,我需要处理显式类型方程。 而显式类型方程是GADTs的领域。典型的GADT确实是等式证明见证。

type ('a,'b) eq = Refl: ('a,'a) eq
let conv (type a b) (Refl: (a,b) eq) (x:a) = (x:b) 

具有类型为 ('a,'b) t 的值 eq 是两种类型相等的证明。例如,我可以使用此类型来证明 m SI.ts SI.t 实际上是相同的类型。

module SI: sig
  type 'a t
  val m: m t
  val s: s t
  val magic: (s t, m t) eq
end = struct
  type 'a t = float
  let m = 1.
  let s = 1.
  let magic = Refl
end

我可以使用这个秘密相等式将秒转换为米(这是不好的)。

let wrong_conversion (x:s SI.t) : m SI.t = 
  conv SI.magic x

因此,我可以使用广义代数数据类型(GADTs)来暴露类型构造函数非单射的事实。同样地,我可以使用单射性注释来证明单射性的另一个等价定义:如果'a t = 'b tt在其第一个参数中是单射的,则'a = 'b
module Inj(T: sig type !'a t end) = struct
  open T
  let conv (type a b) (Refl: (a t, b t) eq): (a,b) eq = Refl
end

这些内容可能略显理论,但是injectivity问题在更实际的问题中也会出现。 假设我有一个容器类型vec
module Vec: sig
  type 'a t
  val v2: 'a -> 'a -> 'a t
end = struct
  type 'a t = 'a * 'a
  let v2 x y = x, y
end
type 'a vec = 'a Vec.t

(现在注意缺少注入性注释) 使用GADTs,我可以通过定义正确的GADT来定义可以处理int vecfloat vec的函数。

type 'a monoid =
  | Int_vec: int vec monoid
  | Float_vec: float vec monoid 

例如,我可以通过幺半群来定义一个zero函数幺半群:
let zero (type a) (kind:a monoid): a = match kind with
| Int_vec -> Vec.v2 0 0
| Float_vec -> Vec.v2 0. 0.

这个操作符的功能正常。问题出现在当我想定义一个仅适用于可能的单子群之一的函数时。例如,只有整数向量有有限数量的半径为1的元素。

let unit_circle (monoid:int vec monoid) = match monoid with
| Int_vec -> Vec.[v2 1 0; v2 (-1) 0; v2 0 1; v2 0 (-1)]

但是,我们收到了一个意外的警告:

警告8 [partial-match]:此模式匹配不是全面的。 以下是一个未匹配的示例: Float_vec

这个警告可能看起来很奇怪,因为Float_vec的类型是float vec monoid,它与int vec monoid类型不匹配,并且尝试将unit_circle应用于Float_vec会导致

let error = unit_circle Float_vec

错误:此表达式具有float vec monoid类型,但需要的是int vec monoid类型的表达式 类型float与int不兼容

因此,类型检查器知道float vecint vec不兼容。 为什么它会警告我们模式匹配中缺失的情况呢? 这个问题涉及到上下文:在分析穷尽性的模式匹配时, 类型检查器看到类型'a vec不是可注入的,因此它不能假设 没有一些隐藏的等式证明实际上float vecint vec是相同的。相反,在应用程序中,类型检查器知道当前上下文中没有这样的等式, 因此它应该拒绝应用程序(即使某个地方确实存在这样的等式)。

我们的简单解决方案是添加缺失的可注入性注释(也可以添加缺失的方差)。

module Vec: sig
  type +!'a t
  val v2: 'a -> 'a -> 'a t
end = struct
  type 'a t = 'a * 'a
  let v2 x y = x, y
end
type 'a vec = 'a Vec.t
type 'a monoid =
  | Int_vec: int vec monoid
  | Float_vec: float vec monoid 

有了这个缺失的信息,类型检查器可以得出结论:一个int vec总是与一个float vec不同,因此我们再也没有警告了。

let unit_circle (monoid:int vec monoid) = match monoid with
| Int_vec -> Vec.[v2 1 0; v2 (-1) 0; v2 0 1; v2 0 (-1)]
| _ -> .

事实上,由于'a vec是单射的,我们知道我们可以从不等式int != float推导出int vec != float vec

关于单射性的结论,单射性注释是库向其用户传达的一种方式,即使类型构造器是抽象的,它仍然保留不等式。这是非常小的信息片段,不会泄露太多信息。同时,它只对通过使用GADTs显式操纵类型方程的客户端有用。


1
感谢您详细的回答!可能需要进行一些编辑:在差异/单射部分添加标题以便更快地访问,并且可能需要简要解释一下GADT和Monoid是什么,即使它们很容易在网络上查找(我对此感到满意,但也许其他人会发现有更多的上下文信息很有用,而且由于您的解释质量很高,每个人都会受益x)) - Felix Bertoni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接