在调用函数和回调之间对齐多态变体类型

Align polymorphic variant types between calling function and callback

我正在尝试编写一个不需要处理所有已知类型事件的事件处理程序,并且正在尝试使用 OCaml 多态变体类型对其进行建模 (event.mli):

type 'events event =
  [< `click of int * int | `keypress of char | `quit of int ] as 'events

(* Main loop should take an event handler callback that handles 'less than' all known event types *)
val mainLoop : ('events event -> unit) -> unit

val handler : 'events event -> unit

示例实现 (event.ml):

type 'events event =
  [< `click of int * int | `keypress of char | `quit of int ] as 'events

let mainLoop handler =
  for n = 1 to 10 do
    handler begin
      if n mod 2 = 0 then `click (n, n + 1)
      else if n mod 3 = 0 then `keypress 'x'
      else `quit 0
    end
  done

let handler = function
  | `click (x, y) -> Printf.printf "Click x: %d, y: %d\n" x y
  | `quit code -> exit code

不幸的是,失败并出现以下错误:

File "event.ml", line 1:
Error: The implementation event.ml
       does not match the interface event.cmi:
       Values do not match:
         val mainLoop :
           ([> `click of int * int | `keypress of char | `quit of int ] -> 'a) ->
           unit
       is not included in
         val mainLoop :
           ([< `click of int * int | `keypress of char | `quit of int ] event ->
            unit) ->
           unit
       File "event.ml", line 4, characters 4-12: Actual declaration

如何将 mainLoop 的实现推断为 ([< `click of int * int | `keypress of char | `quit of int ] -> unit) -> unit 类型,即 ('events event -> unit) -> unit

我认为问题出在你的类型定义中,我知道你希望你的类型最多包含这三个事件(首先为什么是 'at most' 而不是 'at least' ?)但是在这个带有 mainLoop 签名的案例,您无法确定您的类型。

例如看 x 的类型:

let (x : [< `A | `B]) = `A
val x : [< `A | `B > `A ] = `A

[< ... >][< ...]不是一回事。这意味着即使你投mainLoop你也会有一个错误:

let mainLoop (handler :
              [< `click of int * int | `keypress of char | `quit of int ]
              event -> unit) = ...

       Values do not match:
         val mainLoop :
           ([ `click of int * int | `keypress of char | `quit of int ] event ->
            unit) ->
           unit
       is not included in
         val mainLoop :
           ([< `click of int * int | `keypress of char | `quit of int ] event ->
            unit) ->
           unit

但这真的是个问题吗?为什么不将 type 'events event = [< ... 更改为 type 'events event = [ ...

在我看来,使用下限比使用上限更好:

type 'events event =
  [> `click of int * int | `keypress of char | `quit of int ] as 'events

val mainLoop : ('events event -> unit) -> unit

val handler : 'events event -> unit

let mainLoop handler =
  for n = 1 to 10 do
    handler (
      if n mod 2 = 0 then `click (n, n + 1)
      else if n mod 3 = 0 then `keypress 'x'
      else `quit 0
    )
  done

let handler = function
  | `click (x, y) -> Printf.printf "Click x: %d, y: %d\n" x y
  | `quit code -> exit code
  | _ -> ()

让我们用通俗易懂的语言和一些常识对'subtyping theory'进行一些解释。

在面向对象的语言(如 java 或 ocaml)中,您可以定义的最通用的 class 是空的 class,即。 class 没有 属性 也没有方法。实际上,任何 class 都可以从它派生,并且就类型而言,任何 class 类型都是空 class 类型的子类型。

现在,functions 在输入上是 逆变,在输出上是 协变

如果我们查看函数的类型行为,我们会发现:

  • 您可以向它传递一个它接受的类型的值,或者该类型的任何子类型的值。在极端情况下,如果您定义一个接受空 class 实例的函数,显然该函数将无法对其执行任何操作。因此,任何其他 class 的实例也可以,因为我们知道该函数不会期望它的任何内容。
  • 函数的结果可能是它被定义为 returning 的类型的值,或者是该类型的任何子类型的值。

为什么我们对输入和输出看似相同的行为使用两个不同的词?

好吧,现在考虑 ML 风格类型理论中的常用类型构造函数:products(* 类型构造函数)、sums(代数数据类型)和 arrows(函数类型)。基本上,如果您使用乘积或求和定义类型 T,则对它们的任何参数进行特化(使用其子类型)将产生 T 的特化(子类型)。我们称此特征为 covariance。例如,由求和组成的列表构造函数是协变的:如果您有一个 class a 的列表,并且 b 是从 a 派生的,那么类型 b lista list 的子类型。实际上,对于接受 a list 的函数,您可以毫无错误地传递给它 b list

如果我们看一下 箭头 -> 构造函数,情况会略有不同。 x -> y 类型的函数 f 接受 x 的任何子类型和 returns y 的任何子类型。如果我们假设 x 是一个函数类型,这意味着 f 实际上具有类型 (u -> v) -> y,其中 x = u -> v。到目前为止,一切都很好。 uv 在此设置中有何不同?这取决于 f 可以用它做什么。 f 知道它只能将 uu 的子类型传递给该函数,因此 f 可以传递的最一般值是 u,这意味着实际函数是passed 可以接受 u 的任何超类型作为参数。在极端情况下,我可以给 f 一个函数,它接受空对象作为参数和 return 类型 v 的东西。所以突然之间,类型集从 'type and subtypes' 变成了 'type and supertypes'。所以 u -> v 类型的子类型是 u' -> v' 其中 v'v 的子类型并且 u' 超类型 u 个。这就是为什么我们的箭头构造函数据说在其输入上是逆变的。类型构造函数的 方差 是根据其参数的 subtypes/supertypes 确定其 subtypes/supertypes 的方式。

接下来,我们必须考虑多态变体。如果类型 x 被定义为 [ `A | `B ],那么与类型 y = [ `A ] 有什么关系?子类型化的主要 属性 是给定类型的任何值都可以安全地向上转换为超类型(实际上,子类型化通常是这样定义的)。这里,`A 属于两种类型,因此双向转换都是安全的,但 `B 仅出现在第一种类型中,因此可能不会转换为第二种类型。 因此,y 的值可能会转换为第一个,但 x 的值可能不会转换为第二个。子类型关系很清楚:yx 的子类型!

[> ... ][< ...] 符号呢?第一个表示一个类型及其所有超类型(它们有无穷多个),而第二个表示一个类型及其所有子类型(这是一个有限集,包含空类型)。因此,对于采用多态变体 v 的函数自然推断的类型将在该变体及其所有子类型的输入上,即。 [< v ],但是高阶函数——一个以函数作为参数的函数——将看到参数方差翻转,因此它的输入类型将类似于 ([> v ] -> _) -> _。函数差异的确切规则在上面链接的维基百科页面中表达。

现在您可能明白为什么无法构造您的目标类型 - ([< _ ] -> _) -> _)。我们的箭头差异禁止它。

那么,您可以在代码中做什么?我的猜测是,您真正想要做的是推理算法将从您的示例代码中推断出的内容:([> basic_event ] -> _) -> _)。其中 basic_event 类型将恰好涵盖那里的 3 个变体。

type basic_event =
   [ `click of int * int | `keypress of char | `quit of int ]

(* Main loop should take an event handler callback that handles 'less than' 
   all known event types *)
val mainLoop : ([> basic_event ] -> unit) -> unit

val handler : [< `click of int * int | `quit of int ] -> unit

在您的情况下,最好不要在类型中包含下限或上限,并在函数签名中使用这些界限,如上面的代码所述。