7

我想了解 OCAML 对象这种行为的原因。假设我有一个类A调用另一个类的对象的方法B。从原理上讲,A#f 调用 B#g 和 B#h。OOP 中的常规做法是我想避免将 B 用作固定的具体类,而是只为 B 声明一个接口。在 OCAML 中执行此操作的最佳方法是什么?我尝试了几个选项,但我不太明白为什么其中一些有效而另一些无效。以下是代码示例。

版本 1:

 # class classA = object
    method f b = b#g + b#h 
   end ;;
 Error: Some type variables are unbound in this type:
     class a : object method f : < g : int; h : int; .. > -> int end
   The method f has type (< g : int; h : int; .. > as 'a) -> int where 'a
   is unbound

这种行为是众所周知的:OCAML 正确推断出b具有开放对象类型<g:int;h:int;..>,但随后抱怨我的类没有声明任何类型变量。所以似乎需要classA有类型变量;然后我明确地介绍了一个类型变量。

版本 2:

 # class ['a] classA2 = object
   method f (b:'a) = b#g + b#h
   end ;;
 class ['a] classA2 :
   object constraint 'a = < g : int; h : int; .. > method f : 'a -> int end

这可行,但该类现在是具有类型约束的显式多态性,如 OCAML 所示。类类型包含类型变量也令人困惑,'a但我仍然可以说let x = new classA2没有为'a. 为什么会这样?

另一个缺点classA2是显式类型约束(b:'a)包含类型变量。毕竟,我知道它b必须符合一个固定的接口而不是一个未知的类型'a。我希望 OCAML 验证这个接口确实是正确的。

所以在版本 3 中,我首先将接口声明classB为类类型,然后声明它b必须是这种类型:

 # class type classB = object method g:int method h:int end;;
 class type classB = object method g : int method h : int end
 # class classA3 = object method f (b:classB) = b#g + b#h end;;
 class classA3 : object method f : classB -> int end

这也有效,但我的困惑仍然存在:为什么不再classA3需要显式多态性?

问题总结:

  • 为什么即使使用类型变量声明也可以在new classA2不指定类型的情况下使用?'aclassA2'a
  • 为什么classA3接受类型约束(b:classB)并且不再需要绑定类型变量?
  • 的功能是否存在某种微妙的classA2差异classA3,如果是,如何?
4

2 回答 2

6

这会有点复杂,所以请坚持。首先,让我添加一个classA4变体,它恰好是您真正需要的。

class classA4 = object
  method f : 'a. (#classB as 'a) -> int = fun b -> b#g + b#h
end

classA2classA3classA4都是微妙的不同,区别在于OCaml如何对待类型多态和对象多态。让我们假设两个类b1b2实现了该classB类型。

对象多态而言,这意味着b1可以classB使用强制语法将类型表达式强制转换为类型(new b1 :> classB)。这种类型强制丢弃了类型信息(你不再知道对象是 type b1),所以它必须是显式的。

类型多态而言,这意味着b1可以使用类型来代替任何具有约束#classB(或< g : int ; h : int ; .. >)的类型变量。这不会丢弃任何类型信息(因为类型变量被实际类型替换),因此它由类型推断算法执行。

方法f需要classA3一个 type 参数classB,这意味着强制类型转换:

let b = new b1 
let a = new classA3
a # f b (* Type error, expected classB, found b1 *)
a # f (b :> classB) (* Ok ! *)

这也意味着(只要你强制),任何类的实现classB都可以使用。

Method fofclassA2需要一个与 constraint 匹配的类型的参数#classB,但 OCaml 要求这种类型不应该是未绑定的,因此它是在类级别绑定的。这意味着 的每个实例都classA2将接受实现的单个任意类型的参数classB(并且该类型将被类型推断)。

let b1 = new b1 and b2 = new b2
let a  = new classA2 
a # f b1 (* 'a' is type-inferred to be 'b1 classA2' *)
a # f b2 (* Type error, because b1 != b2  *)

重要的是要注意classA3等价于classB classA2,这就是它不需要绑定类型变量的原因,也是它严格不如 表达性的原因classA2

f使用语法为方法classA4提供了显式类型,该'a.语法在方法级别而不是类级别绑定类型变量。它实际上是一个通用量词,这意味着«可以为任何'a实现的类型调用此方法#classB»:

let b1 = new b1 and b2 = new b2
let a  = new classA4
a # f b1 (* 'a is chosen to be b1 for this call *)
a # f b2 (* 'a is chosen to be b2 for this call *)
于 2012-06-24T12:25:47.097 回答
4

有一个比 Victor 稍微简单的解决方案:您不需要class2对类型进行参数化,只需使用类 type classB

class classA2bis = object
  method f (b: classB) = b#g + b#h
end ;;

Victor 的解决方案 ( f : 'a . (#classB as 'a) -> int) 适用于任何作为classB. 两者都具有同样的表现力:正如他解释的那样,使用 Victor 的解决方案,使用的类在调用站点被实例化:a#f b将适用于任何b大于的类型classB,通过隐式多态实例化。使用此解决方案,参数必须是确切的 type classB,因此如果b有更大的 type: ,则必须显式强制它a#f (b :> classB)

因此,两种解决方案都做出了不同的复杂性妥协:使用 Victor 的解决方案,方法定义使用复杂的多态类型,并且调用站点是轻量级的。使用这种同样富有表现力的解决方案,定义更简单,但调用站点必须使用显式强制。由于只有一个定义站点和几个调用站点,因此通常首选在定义方面具有更高的复杂性,这由所谓的专家库设计人员完成。在实践中,您会在野外发现这两种风格,因此了解它们很重要。

一个历史性的评论,回应您在对 Victor 的回复的评论中所说的话:通过显式强制和显式普遍量化的类型变量进行子类型化并不是OCaml 的最新添加。查看ChangesOCaml 发行版的文件;对象系统可以追溯到 OCaml 1.00(大约从 1995 年开始),子类型(带有显式强制转换)从那时起就出现了,并且在 2002 年发布的 OCaml 3.05 中添加了多态方法和结构字段。

编辑:评论提示的评论。您也可以使用对象类型注释而不是类类型编写以下内容:

class classA2bis = object
  method f (b: < g : int; h : int >) = b#g + b#h
end ;;

我只使用classB了您的示例中已经定义的内容,因此使用结构注释并没有多大用处。在这种情况下(类类型用作类型,而不是定义另一个类类型)两者是等价的。它们没有透露任何关于作为参数的对象的实现;b鉴于 OCaml 的对象系统使用结构类型,任何具有这两种方法和正确类型的对象都可以声称适合这种类型注释(可能通过显式子类型步骤);它可能是由其他人定义的,绝对没有参考您自己的类定义。

在 ocaml 对象系统中,对象类型和类类型之间存在相对细微的区别,我不太了解——我不使用面向对象的编程。如果您愿意,可以在参考手册U3 书中了解详细信息。

编辑 2:请注意,#classB as 'a并且通常classB不具有同等的表达能力:当您想要表达类型的不同出现之间的共享时,第一个更复杂的公式很有用。例如,是与 . 非常不同的类型。它严格来说更通用,因为它严格保留了有关返回类型的更多信息。但是,在您的情况下,这两种类型都是等价的,因为类类型只出现一次,因此没有潜在的共享。'a . (#foo as 'a) -> 'afoo -> foo

于 2012-06-24T13:48:23.610 回答