Skip to main content

第 13 章:对象(Objects)

原文:Anil Madhavapeddy and Yaron Minsky, Real World OCaml: Functional Programming for the Masses, Second Edition, Chapter 13。维护者已确认本书为开源书籍,可翻译并发布用于学习研究。

本章由 Leo White 和 Jason Hickey 撰写。

我们已经看到 OCaml 提供了多种组织程序的工具,尤其是模块。此外,OCaml 也支持面向对象编程。它有对象、类,以及相关类型。本章会介绍 OCaml 对象和子类型;下一章“类(Classes)”会介绍类和继承。

什么是面向对象编程(What Is Object-Oriented Programming)

面向对象编程(object-oriented programming,常缩写为 OOP)是一种编程风格,它把计算和数据封装在逻辑上的对象中。每个对象包含一些存储在字段(fields)中的数据,并拥有一些方法(methods)函数,可以针对对象内部的数据调用这些函数;这也叫向对象“发送消息”。对象背后的代码定义称为(class);对象通过用将要用于构造自身的数据调用构造器,从类定义中构造出来。

OOP 与其他风格之间有五个基本区别:

抽象(Abstraction) : 实现细节隐藏在对象中,而外部接口只是公开可访问的方法集合。

动态查找(Dynamic lookup) : 当向对象发送消息时,实际执行哪个方法由对象的实现决定,而不是由程序的某种静态属性决定。换句话说,不同对象可以用不同方式响应同一条消息。

子类型(Subtyping) : 如果对象 a 拥有对象 b 的全部功能,那么任何期望 b 的上下文都可以使用 a

继承(Inheritance) : 一类对象的定义可以被复用,用于产生一种新对象。新的定义可以覆盖某些行为,也可以与父类共享代码。

开放递归(Open recursion) : 对象的方法可以使用一个特殊变量(通常叫 selfthis)调用同一对象中的其他方法。当对象由类创建时,这些调用会使用动态查找,从而允许一个类中定义的方法调用继承自该类的另一个类中定义的方法。

几乎所有著名的现代编程语言都受到 OOP 的影响。如果你用过 C++、Java、C#、Ruby、Python 或 JavaScript,就会见过这些术语。

13.1 OCaml 对象(OCaml Objects)

如果你已经了解 Java 或 C++ 这类语言中的面向对象编程,那么 OCaml 的对象系统可能会让你有点意外。最重要的区别是:对象及其类型与类系统是完全分离的。在 Java 这样的语言中,类名也会用作实例化该类得到的对象类型,而对象类型之间的关系对应继承关系。例如,如果在 Java 中通过继承 Stack 类实现了一个 Deque 类,那么任何期望栈的地方都可以传入双端队列。

OCaml 完全不同。类用于构造对象并支持继承,但类不是类型。对象有自己的对象类型(object types),而且如果你想使用对象,完全不必使用类。下面是一个简单对象的例子:

# open Base;;
# let s = object
val mutable v = [0; 2]

method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None

method push hd =
v <- hd :: v
end;;
val s : < pop : int option; push : int -> unit > = <obj>

这个对象有一个整数列表值 v,一个返回 v 头部的 pop 方法,以及一个把整数加到 v 头部的 push 方法。

对象类型用尖括号 < ... > 包围,其中只包含方法类型。像 v 这样的字段不是对象公开接口的一部分。与对象的一切交互都通过方法完成。方法调用语法使用 # 字符:

# s#pop;;
- : int option = Some 0
# s#push 4;;
- : unit = ()
# s#pop;;
- : int option = Some 4

注意,与函数不同,方法可以有零个参数,因为方法调用会被路由到某个具体对象实例。这也是 pop 方法不需要 unit 参数的原因;如果写成等价的函数式版本,通常就会需要这样的参数。

对象也可以由函数构造。如果想指定对象初始值,可以定义一个函数,它接收该值并返回对象:

# let stack init = object
val mutable v = init

method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None

method push hd =
v <- hd :: v
end;;
val stack : 'a list -> < pop : 'a option; push : 'a -> unit > = <fun>
# let s = stack [3; 2; 1];;
val s : < pop : int option; push : int -> unit > = <obj>
# s#pop;;
- : int option = Some 3

注意,函数 stack 以及返回对象的类型现在使用多态类型 'a。当在具体值 [3; 2; 1] 上调用 stack 时,得到的对象类型与之前相同,其中栈中值的类型是 int

13.2 对象多态(Object Polymorphism)

与多态变体类似,可以在没有显式类型声明的情况下使用方法:

# let area sq = sq#width * sq#width;;
val area : < width : int; .. > -> int = <fun>
# let minimize sq : unit = sq#resize 1;;
val minimize : < resize : int -> unit; .. > -> unit = <fun>
# let limit sq = if (area sq) > 100 then minimize sq;;
val limit : < resize : int -> unit; width : int; .. > -> unit = <fun>

可以看到,对象类型会根据被调用的方法自动推断出来。

如果类型系统看到对同一个方法的不兼容用法,它会报错:

# let toggle sq b : unit =
if b then sq#resize `Fullscreen else minimize sq;;
Line 2, characters 51-53:
Error: This expression has type < resize : [> `Fullscreen ] -> unit; .. >
but an expression was expected of type < resize : int -> unit; .. >
Types for method resize are incompatible

推断出的对象类型中的 .. 是省略号,表示对象可能还有其他未指定的方法。类型 < width : float; .. > 指定了一个对象,它至少必须有一个 width 方法,也可能还有其他方法。这类对象类型称为开放的(open)。

可以用类型标注手动关闭(close)对象类型:

# let area_closed (sq: < width : int >) = sq#width * sq#width;;
val area_closed : < width : int > -> int = <fun>
# let sq = object
method width = 30
method name = "sq"
end;;
val sq : < name : string; width : int > = <obj>
# area_closed sq;;
Line 1, characters 13-15:
Error: This expression has type < name : string; width : int >
but an expression was expected of type < width : int >
The second object type has no method name

省略号是多态的(Elisions Are Polymorphic)

开放对象类型中的 .. 是一种省略,表示“可能有更多方法”。从语法上看也许不明显,但带省略号的对象类型实际上是多态的。例如,如果尝试写一个类型定义,就会得到“未绑定类型变量”的错误:

# type square = < width : int; ..>;;
Line 1, characters 1-33:
Error: A type variable is unbound in this type declaration.
In type < width : Base.int; .. > as 'a the variable 'a is unbound

这是因为 .. 实际上是一种特殊的类型变量,称为行变量(row variable)。

这种使用行变量的类型方案称为行多态(row polymorphism)。行多态也用于多态变体类型,而对象和多态变体之间有密切关系:对象之于记录,正如多态变体之于普通变体。

类型为 < pop : int option; .. > 的对象可以是任何拥有方法 pop : int option 的对象;它具体如何实现并不重要。当调用方法 #pop 时,实际运行哪个方法由对象决定。考虑下面这个函数:

# let print_pop st = Option.iter ~f:(Stdio.printf "Popped: %d\n") st#pop;;
val print_pop : < pop : int option; .. > -> unit = <fun>

可以在前面定义的基于链表的栈类型上运行它。

# print_pop (stack [5;4;3;2;1]);;
Popped: 5
- : unit = ()

但也可以用 Base 中基于数组的 Stack 模块,创建完全不同的栈实现。

# let array_stack l = object
val stack = Stack.of_list l
method pop = Stack.pop stack
end;;
val array_stack : 'a list -> < pop : 'a option > = <fun>

尽管实现完全不同,print_pop 对这种栈对象也同样适用。

# print_pop (array_stack [5;4;3;2;1]);;
Popped: 5
- : unit = ()

13.3 不可变对象(Immutable Objects)

许多人认为面向对象编程本质上是命令式的:对象像状态机,向对象发送消息会导致它改变状态,并且可能向其他对象发送消息。

确实,在许多程序中这种模型是合理的,但这绝不是必需的。下面定义一个函数,用来创建不可变栈对象:

# let imm_stack init = object
val v = init

method pop =
match v with
| hd :: tl -> Some (hd, {< v = tl >})
| [] -> None

method push hd =
{< v = hd :: v >}
end;;
val imm_stack :
'a list -> (< pop : ('a * 'b) option; push : 'a -> 'b > as 'b) = <fun>

这个实现的关键部分在 poppush 方法中。表达式 {< ... >} 会产生当前对象的一个副本,类型相同,但指定字段被更新。换句话说,push hd 方法会产生该对象的副本,其中 v 被替换为 hd :: v。原始对象不会被修改:

# let s = imm_stack [3; 2; 1];;
val s : < pop : (int * 'a) option; push : int -> 'a > as 'a = <obj>
# let r = s#push 4;;
val r : < pop : (int * 'a) option; push : int -> 'a > as 'a = <obj>
# s#pop;;
- : (int * (< pop : 'a; push : int -> 'b > as 'b)) option as 'a =
Some (3, <obj>)
# r#pop;;
- : (int * (< pop : 'a; push : int -> 'b > as 'b)) option as 'a =
Some (4, <obj>)

表达式 {< ... >} 的使用有一些限制。它只能在方法体内部使用,而且只能更新字段的值。方法实现会在对象创建时固定下来,不能动态改变。

13.4 何时使用对象(When to Use Objects)

你可能会想,OCaml 已经有大量替代机制可以表达类似概念,那么什么时候应该使用对象?一等模块表达力更强,因为模块可以包含类型,而类和对象不能。模块、函子和数据类型也提供了大量表达程序结构的方式。事实上,许多经验丰富的 OCaml 程序员很少使用类和对象,甚至完全不用。

相较于记录,对象有一些优点:它们不需要类型定义,并且对行多态的支持让它们更灵活。然而,对象语法较重,运行时成本也更高,因此很少用它们替代记录。

对象真正的好处来自类系统。类支持继承和开放递归。开放递归允许对象中彼此依赖的部分分开定义。之所以能做到这一点,是因为对象方法之间的调用会在对象实例化时决定,这是一种后期绑定(late binding)。这使得一个方法可以引用对象中的其他方法,而不必静态知道那些方法将如何实现。

相比之下,模块使用早期绑定(early binding)。如果想对模块代码做参数化,让其中某个部分稍后再实现,就需要写函数或函子。这更显式,但往往也比在类中覆盖方法更啰嗦。

一般来说,一个经验法则是:在开放递归能带来巨大收益的场景中使用类和对象。两个不错的例子是 Xavier Leroy 的 Cryptokit,它提供了多种可以像构件一样组合的密码学原语;以及 Camlimages 库,它用于处理多种图形文件格式。Camlimages 还提供同一库的基于模块版本,让你可以根据问题领域选择函数式风格或面向对象风格。

下一章“类(Classes)”会介绍类,以及使用开放递归的示例。

13.5 子类型(Subtyping)

子类型是面向对象编程中的核心概念。它规定了什么时候对象类型 A 的对象可以用于期望另一种对象类型 B 的表达式中。当这成立时,我们说 AB子类型(subtype)。更具体地说,子类型限制了强制转换运算符 e :> t 何时可以应用。只有当 e 的类型是 t 的子类型时,这种强制转换才有效。

13.5.1 宽度子类型(Width Subtyping)

为了探索这一点,先为几何形状定义一些简单对象类型。泛型类型 shape 只有一个用于计算面积的方法。

# type shape = < area : float >;;
type shape = < area : float >

现在添加一个表示特定形状的类型,以及一个创建这种类型对象的函数。

# type square = < area : float; width : int >;;
type square = < area : float; width : int >

# let square w = object
method area = Float.of_int (w * w)
method width = w
end;;
val square : int -> < area : float; width : int > = <fun>

squareshape 一样有 area 方法,并且还有额外的 width 方法。即便如此,我们仍然期望 square 是一种 shape,事实也确实如此。不过注意,强制转换 :> 必须显式写出:

# (square 10 : shape);;
Line 1, characters 2-11:
Error: This expression has type < area : float; width : int >
but an expression was expected of type shape
The second object type has no method width
# (square 10 :> shape);;
- : shape = <obj>

这种形式的对象子类型称为宽度子类型(width subtyping)。宽度子类型意味着:如果对象类型 A 拥有 B 的全部方法,也可能还有更多方法,那么 AB 的子类型。squareshape 的子类型,因为它实现了 shape 的所有方法,在这个例子中也就是 area 方法。

13.5.2 深度子类型(Depth Subtyping)

对象也可以使用深度子类型(depth subtyping)。如果对象中各个方法本身可以安全地被强制转换,那么深度子类型允许我们强制转换该对象。因此,如果 t1t2 的子类型,那么对象类型 < m: t1 > 就是 < m: t2 > 的子类型。

首先,添加一种新的形状类型 circle

# type circle = < area : float; radius : int >;;
type circle = < area : float; radius : int >

# let circle r = object
method area = 3.14 *. (Float.of_int r) **. 2.0
method radius = r
end;;
val circle : int -> < area : float; radius : int > = <fun>

利用它,创建几个对象,每个对象都有一个 shape 方法,其中一个返回 circle 类型的形状:

# let coin = object
method shape = circle 5
method color = "silver"
end;;
val coin : < color : string; shape : < area : float; radius : int > > = <obj>

另一个返回 square 类型的形状:

# let map = object
method shape = square 10
end;;
val map : < shape : < area : float; width : int > > = <obj>

这两个对象都有一个 shape 方法,而该方法的类型都是 shape 类型的子类型,因此它们都可以被强制转换成对象类型 < shape : shape >

# type item = < shape : shape >;;
type item = < shape : shape >
# let items = [ (coin :> item) ; (map :> item) ];;
val items : item list = [<obj>; <obj>]

多态变体子类型(Polymorphic Variant Subtyping)

子类型也可以用于把一个多态变体强制转换成更大的多态变体类型。如果多态变体类型 A 的标签是 B 标签的子集,那么 A 就是 B 的子类型:

# type num = [ `Int of int | `Float of float ];;
type num = [ `Float of float | `Int of int ]
# type const = [ num | `String of string ];;
type const = [ `Float of float | `Int of int | `String of string ]
# let n : num = `Int 3;;
val n : num = `Int 3
# let c : const = (n :> const);;
val c : const = `Int 3

13.5.3 型变(Variance)

那么,由对象类型构造出来的类型又如何?如果 squareshape,我们会期待 square list 也是 shape list。OCaml 的确允许这样的强制转换:

# let squares: square list = [ square 10; square 20 ];;
val squares : square list = [<obj>; <obj>]
# let shapes: shape list = (squares :> shape list);;
val shapes : shape list = [<obj>; <obj>]

注意,这依赖于列表是不可变的。把 square array 当作 shape array 并不安全,因为这会允许你把非正方形的形状存入本应只包含正方形的数组。OCaml 能识别这一点,因此不允许这种强制转换:

# let square_array: square array = [| square 10; square 20 |];;
val square_array : square array = [|<obj>; <obj>|]
# let shape_array: shape array = (square_array :> shape array);;
Line 1, characters 32-61:
Error: Type square array is not a subtype of shape array
The second object type has no method width

我们说 'a list(在 'a 上)是协变的(covariant),而 'a array不变的(invariant)。

函数类型的子类型还需要第三类型变。类型为 square -> string 的函数不能用作类型 shape -> string,因为它期望参数是 square,不知道该如何处理 circle。然而,类型为 shape -> string 的函数可以安全地用作 square -> string

# let shape_to_string: shape -> string =
fun s -> Printf.sprintf "Shape(%F)" s#area;;
val shape_to_string : shape -> string = <fun>
# let square_to_string: square -> string =
(shape_to_string :> square -> string);;
val square_to_string : square -> string = <fun>

我们说 'a -> string'a 上是逆变的(contravariant)。一般来说,函数类型在参数上逆变,在结果上协变。

型变标注(Variance Annotations)

OCaml 会根据类型定义来推断一个类型的型变。考虑下面这个简单的不可变 Either 类型:

# module Either = struct
type ('a, 'b) t =
| Left of 'a
| Right of 'b
let left x = Left x
let right x = Right x
end;;
module Either :
sig
type ('a, 'b) t = Left of 'a | Right of 'b
val left : 'a -> ('a, 'b) t
val right : 'a -> ('b, 'a) t
end

通过观察哪些强制转换被允许,可以看到不可变 Either 类型的类型参数是协变的。

# let left_square = Either.left (square 40);;
val left_square : (< area : float; width : int >, 'a) Either.t =
Either.Left <obj>
# (left_square :> (shape,_) Either.t);;
- : (shape, 'a) Either.t = Either.Left <obj>

不过,如果类型定义被签名隐藏,情况就不同了。

# module Abs_either : sig
type ('a, 'b) t
val left: 'a -> ('a, 'b) t
val right: 'b -> ('a, 'b) t
end = Either;;
module Abs_either :
sig
type ('a, 'b) t
val left : 'a -> ('a, 'b) t
val right : 'b -> ('a, 'b) t
end

在这种情况下,OCaml 被迫假设该类型是不变的。

# (Abs_either.left (square 40) :> (shape, _) Abs_either.t);;
Line 1, characters 2-29:
Error: This expression cannot be coerced to type (shape, 'b) Abs_either.t;
it has type (< area : float; width : int >, 'a) Abs_either.t
but is here used with type (shape, 'b) Abs_either.t
Type < area : float; width : int > is not compatible with type
shape = < area : float >
The second object type has no method width

可以通过在签名中为类型参数添加型变标注来修复这个问题:+ 表示协变,- 表示逆变:

# module Var_either : sig
type (+'a, +'b) t
val left: 'a -> ('a, 'b) t
val right: 'b -> ('a, 'b) t
end = Either;;
module Var_either :
sig
type (+'a, +'b) t
val left : 'a -> ('a, 'b) t
val right : 'b -> ('a, 'b) t
end

可以看到,现在这种强制转换又被允许了。

# (Var_either.left (square 40) :> (shape, _) Var_either.t);;
- : (shape, 'a) Var_either.t = <abstr>

来看一个更具体的型变例子。先把 stack 函数应用到一些正方形和圆上,创建一些包含形状的栈:

# type 'a stack = < pop: 'a option; push: 'a -> unit >;;
type 'a stack = < pop : 'a option; push : 'a -> unit >
# let square_stack: square stack = stack [square 30; square 10];;
val square_stack : square stack = <obj>
# let circle_stack: circle stack = stack [circle 20; circle 40];;
val circle_stack : circle stack = <obj>

如果想写一个函数,接收这类栈的列表,并计算其中所有形状的总面积,可能会先尝试这样写:

# let total_area (shape_stacks: shape stack list) =
let stack_area acc st =
let rec loop acc =
match st#pop with
| Some s -> loop (acc +. s#area)
| None -> acc
in
loop acc
in
List.fold ~init:0.0 ~f:stack_area shape_stacks;;
val total_area : shape stack list -> float = <fun>

然而,当尝试把这个函数应用到对象上时,会得到错误:

# total_area [(square_stack :> shape stack); (circle_stack :> shape stack)];;
Line 1, characters 13-42:
Error: Type square stack = < pop : square option; push : square -> unit >
is not a subtype of
shape stack = < pop : shape option; push : shape -> unit >
Type shape = < area : float > is not a subtype of
square = < area : float; width : int >
The first object type has no method width

可以看到,square stackcircle stack 并不是 shape stack 的子类型。问题出在 push 方法上。对于 shape stackpush 方法接收任意 shape。如果可以把 square stack 强制转换成 shape stack,那么就能把任意形状压入 square stack,这显然是错误的。

换个角度看,< push: 'a -> unit; .. >'a 上是逆变的,因此 < push: square -> unit; pop: square option > 不可能是 < push: shape -> unit; pop: shape option > 的子类型。

不过,原则上 total_area 函数应该是没问题的。它没有调用 push,所以不会犯这种错误。为了让它工作,需要使用更精确的类型,说明我们不会使用 push 方法。下面定义类型 readonly_stack,并确认可以把 stack 列表强制转换到它:

# type 'a readonly_stack = < pop : 'a option >;;
type 'a readonly_stack = < pop : 'a option >
# let total_area (shape_stacks: shape readonly_stack list) =
let stack_area acc st =
let rec loop acc =
match st#pop with
| Some s -> loop (acc +. s#area)
| None -> acc
in
loop acc
in
List.fold ~init:0.0 ~f:stack_area shape_stacks;;
val total_area : shape readonly_stack list -> float = <fun>
# total_area [(square_stack :> shape readonly_stack); (circle_stack :>
shape readonly_stack)];;
- : float = 7280.

本节的部分内容可能看起来有些复杂,但应该指出:这种类型机制确实可用,而且最终所需类型标注相当少。在多数带类型的面向对象语言中,这些强制转换根本无法实现。例如在 C++ 中,STL 类型 list<T>T 上是不变的,所以在期望 list<shape> 的地方无法安全使用 list<square>。Java 的情况类似,尽管 Java 有一个逃生口,可以让程序退回到动态类型。OCaml 的情况要好得多:它能工作,受静态检查,而且标注相当简单。

13.5.4 收窄(Narrowing)

收窄(narrowing),也叫向下转型(down casting),是把对象强制转换为其某个子类型的能力。例如,如果有一个形状列表 shape list,我们可能因为某种原因知道每个形状的实际类型。也许我们知道列表中所有对象都具有类型 square。在这种情况下,收窄会允许我们把对象从类型 shape 重新转换成类型 square。许多语言通过动态类型检查支持收窄。例如在 Java 中,如果值 x 的类型是 Square 或其某个子类型,那么强制转换 (Square) x 是允许的;否则该强制转换会抛出异常。

OCaml 中不允许收窄。就是这样。

为什么?有两个合理解释:一个基于设计原则,另一个是技术原因。技术原因很简单:它很难实现。

设计上的论点是:收窄违反抽象。事实上,在 OCaml 这样的结构化类型系统中,收窄本质上会提供枚举对象方法的能力。要检查某个对象 obj 是否有方法 foo : int,可以尝试执行强制转换 (obj :> < foo : int >)

更实际地说,收窄会导致糟糕的面向对象风格。考虑下面这段 Java 代码,它返回一个形状对象的名称:

String GetShapeName(Shape s) {
if (s instanceof Square) {
return "Square";
} else if (s instanceof Circle) {
return "Circle";
} else {
return "Other";
}
}

大多数程序员至少会认为这段代码别扭。与其对对象类型做分类讨论,不如定义一个方法来返回形状名称。也就是说,不应该调用 GetShapeName(s),而应该调用 s.Name()

不过,情况并不总是这么明显。下面的代码检查一个形状数组是否看起来像哑铃:两个 Circle 对象之间夹着一个 Line,并且两个圆半径相同。

boolean IsBarbell(Shape[] s) {
return s.length == 3 && (s[0] instanceof Circle) &&
(s[1] instanceof Line) && (s[2] instanceof Circle) &&
((Circle) s[0]).radius() == ((Circle) s[2]).radius();
}

在这种情况下,要如何增强 Shape 类来支持这种模式分析,就不那么清楚了。面向对象编程是否适合这种场景,也并不明显。模式匹配看起来更合适:

# type shape = Circle of { radius : int } | Line of { length: int };;
type shape = Circle of { radius : int; } | Line of { length : int; }
# let is_barbell = function
| [Circle {radius=r1}; Line _; Circle {radius=r2}] when r1 = r2 -> true
| _ -> false;;
val is_barbell : shape list -> bool = <fun>

无论如何,如果确实遇到这种情况,也有一个解决方案:用变体增强类。可以定义一个 variant 方法,把实际对象注入到一个变体类型中。

# type shape = < variant : repr >
and circle = < variant : repr; radius : int >
and line = < variant : repr; length : int >
and repr =
| Circle of circle
| Line of line;;
type shape = < variant : repr >
and circle = < radius : int; variant : repr >
and line = < length : int; variant : repr >
and repr = Circle of circle | Line of line
# let is_barbell = function
| [s1; s2; s3] ->
(match s1#variant, s2#variant, s3#variant with
| Circle c1, Line _, Circle c2 when c1#radius = c2#radius -> true
| _ -> false)
| _ -> false;;
val is_barbell : < variant : repr; .. > list -> bool = <fun>

这种模式可以工作,但也有缺点。尤其是,这个递归类型定义应该清楚地说明:这种模式本质上等价于使用变体,而对象在这里并没有提供多少价值。

13.5.5 子类型与行多态(Subtyping Versus Row Polymorphism)

子类型和行多态之间有相当多重叠。两种机制都允许你编写可应用于不同类型对象的函数。在这些场景中,行多态通常优于子类型,因为它不需要显式强制转换,而且会保留更多类型信息,从而允许写出如下函数:

# let remove_large l =
List.filter ~f:(fun s -> Float.(s#area <= 100.)) l;;
val remove_large : (< area : float; .. > as 'a) list -> 'a list = <fun>

这个函数的返回类型基于参数的开放对象类型构造,会保留参数可能拥有的额外方法,如下所示。

# let squares : < area : float; width : int > list =
[square 5; square 15; square 10];;
val squares : < area : float; width : int > list = [<obj>; <obj>; <obj>]
# remove_large squares;;
- : < area : float; width : int > list = [<obj>; <obj>]

如果用闭合类型写一个类似函数,并使用子类型来应用它,就不会保留参数的方法:返回对象只知道有一个 area 方法。

# let remove_large (l: < area : float > list) =
List.filter ~f:(fun s -> Float.(s#area <= 100.)) l;;
val remove_large : < area : float > list -> < area : float > list = <fun>
# remove_large (squares :> < area : float > list );;
- : < area : float > list = [<obj>; <obj>]

有些情况下不能使用行多态。特别是,行多态不能用于把不同类型的对象放入同一个容器。例如,不能用行多态创建异构元素列表:

# let hlist: < area: float; ..> list = [square 10; circle 30];;
Line 1, characters 50-59:
Error: This expression has type < area : float; radius : int >
but an expression was expected of type < area : float; width : int >
The second object type has no method radius

类似地,也不能用行多态把不同类型的对象存入同一个引用:

# let shape_ref: < area: float; ..> ref = ref (square 40);;
val shape_ref : < area : float; width : int > ref =
{Base.Ref.contents = <obj>}
# shape_ref := circle 20;;
Line 1, characters 14-23:
Error: This expression has type < area : float; radius : int >
but an expression was expected of type < area : float; width : int >
The second object type has no method radius

在这两种情况下,都必须使用子类型:

# let hlist: shape list = [(square 10 :> shape); (circle 30 :> shape)];;
val hlist : shape list = [<obj>; <obj>]
# let shape_ref: shape ref = ref (square 40 :> shape);;
val shape_ref : shape ref = {Base.Ref.contents = <obj>}
# shape_ref := (circle 20 :> shape);;
- : unit = ()