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

时间:2017-09-03 22:57:16

标签: types ocaml algebraic-data-types subtyping

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

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

2 个答案:

答案 0 :(得分:3)

我认为问题出在你的类型定义中,我知道你希望你的类型至多包含这三个事件(首先为什么'最多'而不是'至少'?)但是在这种情况下使用这个签名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
  | _ -> ()

答案 1 :(得分:2)

让我们用简单的英语和一些常识来解释“亚型理论”。

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

现在,functions在输入上被称为逆变,在输出上被称为协变

如果我们看一下函数的行为方式,我们会看到:

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

为什么我们使用两个不同的单词来表示输入和输出的相同行为?

好吧,现在考虑ML样式类型理论中常用的类型构造函数:products(*类型构造函数),sums(代数数据类型)和箭头(函数类型)。基本上,如果使用产品或总和定义类型T,则特殊化(使用子类型)任何参数都将产生T的特化(子类型)。我们将此特征称为协方差。例如,由sums组成的列表构造函数是协变的:如果您有一个类a的列表,而b是从a派生的,那么类型为{{ 1}}是b list的子类型。实际上,对于接受a list的函数,您可以传递a list而不会出错。

如果我们查看 arrow b list构造函数,那么故事就会略有不同。类型->的函数f采用x -> y的任何子类型并返回x的任何子类型。如果我们假设x是一个函数类型,则意味着f实际上是y(u -> v) -> y = x。到现在为止还挺好。在这种情况下,u -> vu如何变化?这取决于f可以用它做什么。 f知道它只能将vu的子类型传递给该函数,因此f可以传递的最一般值是u,这意味着实际的函数是传递可以接受u的任何超类型作为参数。在极端情况下,我可以给f一个接受空对象作为其参数的函数,并返回u类型的函数。所以突然间,类型集从“类型和子类型”变为“类型和超类型”。因此,v类型的子类型为u -> v,其中u' -> v'v'的子类型,v超类型 { {1}}。这就是为什么我们的箭头构造函数被认为是对其输入的逆变。类型构造函数的方差是如何根据其参数的子类型/超类型确定其子类型/超类型。

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

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

现在你可以看到为什么你的目标类型 - [< v ] - 无法构建。箭头的差异禁止它。

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

([> basic_event ] -> _) -> _)

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