OCaml的对象系统-2
1 简介
这篇文章是Introduction to Objective Caml第14, 15, 16, 17章关于对象系 统的笔记,并不是完整翻译。
2 对象
面向对象编程是一个基于"对象"和它们的交互的编程模型。OCaml的对象系统 与其它语言有几点不同。它相当有表现力,并且它是结构上的而不是名义上的, 也就是说对象和类有一点不同。 本章从简单对象开始,没有类。
一开始,可以简单地将对象认为是一组数据和操作这些数据的函数的集合。这 些数据叫做对象的字段(fields),这些函数叫做方法(methods)。例如,下面 的对象表示了一个多边形,包含一个 draw 方法用来在屏幕上绘制。
#load "graphics.cma";; # let poly = object val vertices = [| (46, 70); (54, 70); (60, 150); (40, 150)|] method draw = Graphics.fill_poly vertices end;; val poly : < draw : unit > = <obj>
对象类型包含方法类型,但是没有字段的类型。定义另一个对象:
# let circle = object val center = (50, 50) val radius = 10 method draw = let x, y = center in Graphics.fill_circle x y radius end;; val circle : < draw : unit > = <obj>
方法通过 object#method-name 调用(这也叫做发送一个消息给对象)。 下面的操作打开一个图形窗口,绘制这两个对象。
let graph = Graphics.open_graph " 200x200";; poly#draw;; circle#draw;;
这个示例演示了面向对象编程的一个属性:动态查找(dynamic lookup)1。一个表达式 obj#m ,实际的调用方法 m 由对象 obj 动态决定。
执行结果
2.1 封装和多态
另一个面向对象编程的重要特性是封装(encapsulation),也叫做抽象 (abstraction)。一个对象封装一些数据和操作这些数据的方法;并不需要知 道对象是如何实现的就能使用它。
# let draw_list items = List.iter (fun item -> item#draw) items;; val draw_list : < draw : unit; .. > list -> unit = <fun> # draw_list [poly; circle];; - : unit = ()
注意函数 draw_list 的类型,它指定接受一个对象类型 < draw : unit; .. > 的列表。 省略号 .. 表示其它方法。也就是说, draw_list 函 数接受一个至少有一个 draw : unit 方法的对象列表。假设我们定义一个新对 象表示正方形,除了拥有draw方法,也定义一个area方法计算面积。
# let square = object val lower_left = (100, 100) val width = 10 method area = width * width method draw = let (x, y) = lower_left in Graphics.fill_rect x y width width end;; val square : < area : int; draw : unit > = <obj> # draw_list [square];; - : unit = ()
执行结果2
如果我们使用更简单的类型 draw_list_exact : < draw : unit >list -> unit ,这个函数将只能在只拥有一个方法的对象上工作,这个方法必须是 draw .
# let draw_list_exact (items : < draw : unit > list) = List.iter (fun item -> item#draw) items;; val draw_list_exact : < draw : unit > list -> unit = <fun> # draw_list_exact [square];; Characters 17-23: draw_list_exact [square];; ^^^^^^ Error: This expression has type < area : int; draw : unit > but an expression was expected of type < draw : unit > The second object type has no method area
技术上来说,一个对象类型中的省略号 .. 叫做行变量(row variable), 这 样的类型组合叫做行多态(row polymorphism).它看起来不像这样,但是这个类型 真正是多态的,例如写如下类型定义:
# type blob = < draw : unit; ..>;; Characters 5-30: type blob = < draw : unit; ..>;; ^^^^^^^^^^^^^^^^^^^^^^^^^ Error: A type variable is unbound in this type declaration. In type < draw : unit; .. > as 'a the variable 'a is unbound
这个问题是因为省略号 .. 就像一个类型变量,用来为这个类型的其它方法提 供位置.不幸的是,它并不是一个类型变量,不能这样写: type (..) blob = < draw : unit; .. >.这个错误消息有一点模糊,但是它提出了解决方案,通 过引入一个类型变量'a来为整个对象的类型提供位置. as 形式和 constraint 形式是相等的.
# type 'a blob = < draw : unit; .. > as 'a;; type 'a blob = 'a constraint 'a = < draw : unit; .. > # let draw_list_poly : 'a blob list -> unit = draw_list;; val draw_list_poly : < draw : unit; .. > blob list -> unit = <fun> # draw_list_poly [square];; - : unit = ()
2.2 转换
2D图形库的一个重要特性是通过比例,旋转,或转换来改变对象的能力.这样做 的最清晰的方法是使用统一坐标系,一个2D坐标(x, y)表示为(x, y, 1),转换 表示为3x3的转换矩阵.这个3x3矩阵拥有9个值.
2.2.1 基本转换
2.2.2 函数式更新
let transform = object val matrix = (1., 0., 0., 0., 1., 0.) method new_scale sx sy = {< matrix = (sx, 0., 0., 0., sy, 0.) >} method new_rotate theta = let s, c = sin theta, cos theta in {< matrix = (c, -.s, 0., s, c, 0.) >} method new_translate dx dy = {< matrix = (1., 0., dx, 0., 1., dy) >} method transform (x, y) = ... method multiply matrix2 = ... end;;
表达式 {< … >} 表示一个函数式更新.这种更新产生一个和当前对 象相同的新对象,除了指定的修改;原始的对象不会受影响.
一般来说,使用一个函数式更新时要记住两点:一是表达式 {< … >} 只 能在方法中使用. 二是只能用来更新字段,不能更新方法–方法的实现在对象 创建时就固定了.
2.3 二元方法
transform 方法只是一个矩阵相乘,计算矩阵与向量相乘:
method transform (x, y) = let (m11, m12, m13, m21, m22, m23) = matrix in (m11 *. x +. m12 *. y +. m13, m21 *. x +. m22 *. y +. m23)
multiply 方法有一点困难,主要是因为OCaml不像其它面向对象语言,对象 中的字段是私有的. multiply 叫做二元方法,因为它接受同样类型的另一 个对象作为参数.一个二元方法不能直接访问作为参数传递的对象的字段.
有几种方法来完成二元方法,但是最简单的是通过添加一个表示方法来暴露对象的 内部表示.完整的实现如下所示:
# let transform = object val matrix = (1., 0., 0., 0., 1., 0.) method new_scale sx sy = {< matrix = (sx, 0., 0., 0., sy, 0.) >} method new_rotate theta = let s, c = sin theta, cos theta in {< matrix = (c, -.s, 0., s, c, 0.) >} method new_translate dx dy = {< matrix = (1., 0., dx, 0., 1., dy) >} method transform (x, y) = let (m11, m12, m13, m21, m22, m23) = matrix in (m11 *. x +. m12 *. y +. m13, m21 *. x +. m22 *. y +. m23) method representation = matrix method multiply matrix2 = let (x11, x12, x13, x21, x22, x23) = matrix in let (y11, y12, y13, y21, y22, y23) = matrix2#representation in {< matrix = (x11 *. y11 +. x12 *. y21, x11 *. y12 +. x12 *. y22, x11 *. y13 +. x12 *. y23 +. x13, x21 *. y11 +. x22 *. y21, x21 *. y12 +. x22 *. y22, x21 *. y13 +. x22 *. y23 +. x23) >} end;; val transform : < multiply : < representation : float * float * float * float * float * float; .. > -> 'a; new_rotate : float -> 'a; new_scale : float -> float -> 'a; new_translate : float -> float -> 'a; representation : float * float * float * float * float * float; transform : float * float -> float * float > as 'a = <obj> ;; # let ( ** ) t1 t2 = t1#multiply t2;; val ( ** ) : < multiply : 'a -> 'b; .. > -> 'a -> 'b = <fun>
2.4 对象工厂
每次创建一个对象都要进行定义很麻烦,所以我们写一个函数来创建新对象 (创建新对象的函数常叫做工厂).
# let int_coord (x, y) = (int_of_float x, int_of_float y);; val int_coord : float * float -> int * int = <fun> ;; # let new_poly vertices = object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform matrix = {< vertices = Array.map matrix#transform vertices >} end;; val new_poly : (float * float) array -> (< draw : unit; transform : < transform : float * float -> float * float; .. > -> 'a > as 'a) = <fun> ;; let new_circle center radius = object val center = center val radius = radius method draw = let x, y = int_coord center in Graphics.fill_circle x y (int_of_float radius) method transform matrix = (* 半径的transform 需要修改,这里使用固定值5. *) let r = 5. in (* let (r, _) = matrix#transform (radius, radius) in *) let (x, y) as center = matrix#transform center in Printf.printf "center:%f, %f. radius:%f\n" x y r; {< center = center; radius = r >} end;; val new_circle : float * float -> float -> (< draw : unit; transform : < transform : float * float -> float * float; .. > -> 'a > as 'a) = <fun>
演示图形库:
let poly = new_poly [| (-0.05, 0.2); (0.05, 0.2); (0.1, 1.0); (-0.1, 1.0) |] in let circle = new_circle (0.0, 0.0) 0.1 in let matrix1 = (transform#new_translate 50.0 50.0) ** (transform#new_scale 100.0 100.0) in for i = 0 to 9 do let matrix2 = matrix1 ** (transform#new_rotate (0.628 *. float_of_int i)) in (poly#transform matrix2)#draw done; (circle#transform matrix1)#draw;;
程序从两个对象 poly 和 circle 开始,以0点为中心. matrix1 放大 100倍并移动图片的中心到(50, 50). matrix2 转换沿着点(50, 50)呈放射 状旋转绘制.
执行结果3
2.5 指令式对象
let new_collection () = object val mutable items = [] method add item = items <- item :: items method draw = print_endline "draws..."; List.iter (fun item -> item#draw) items method transform matrix = {< items = List.map (fun item -> item#transform matrix) items >} end;; val new_collection : unit -> (< add : (< draw : unit; transform : 'c -> 'b; .. > as 'b) -> unit; draw : unit; transform : 'c -> 'a > as 'a) = <fun>
撇开推导出的类型,这个定义很简单. items 字段声明为可变的, add 方 法是用有副作用的方式修改它(使用<-进行赋值).构造一个star:
let matrix1 = (transform#new_translate 100.0 80.0) ** (transform#new_scale 100.0 100.0) let star = let poly = new_poly [| (0.0, 0.2); (0.1, 0.5); (0.0, 1.0); (-0.1, 0.5) |] in let star = new_collection () in star#add ((new_circle (0.0, 0.0) 0.1)#transform matrix1); for i = 0 to 9 do let trans = matrix1 ** transform#new_rotate (0.628 *. (float_of_int i)) in star#add (poly#transform trans) done; star;; let starry_night = let starry_night = new_collection () in let add_star (x, y, scale) = let trans = (transform#new_translate x y) ** (transform#new_scale scale scale) in starry_night#add (star#transform trans) in List.iter add_star [35., 50., 0.15; 12., 95., 0.12; 35., 95., 0.10; 62., 90., 0.12; 95., 85., 0.20]; starry_night ;; star#draw;; starry_night#draw;; let graph = Graphics.open_graph " 200x200";;
star
starry_night
2.6 self:引用当前对象
假如我们希望定义一个递归方法,或者一个方法调用同一个对象中的另一个 方法。在OCaml中,一个对象的字段可以直接通过名字引用,但是方法必须总 是通过 object#method-name 的形式调用。如果我们希望在当前对象中调 用一个方法,对象必须使用下面的语法命名:
object (pattern) ... end
pattern可以使用任何标识符,但是按照约定当前对象总是命名为 self 。 也可以用来指定一个类型。下面的 self 表示当前对象,'self表示它 的类型。
object (self : 'self) ... end
现在有了当前对象的名字,可以用来定义一个集合方法 add_multiple n trans item .
let new_collection () = object (self : 'self) val mutable items = [] method add item = items <- item :: items method add_mutiple n matrix item = if n > 0 then ( self#add item; self#add_mutiple (n - 1) matrix (item#transform matrix)) method draw = print_endline "draws..."; List.iter (fun item -> item#draw) items method transform matrix = {< items = List.map (fun item -> item#transform matrix) items >} end;; let line = new_poly [| (0., 0.); (2., 0.); (2., 30.); (0., 30.) |] ;; let xform = transform#new_translate 3. 0. ** transform#new_scale 1.1 1.1;; let image = new_collection ();; image#add_mutiple 25 xform line;; image#draw;;
add_multiple
2.7 初始化器;私有方法
这是一条构造对象时需要牢记的重要的规则:字段表达式不能引用其它字段, 也不能引用self。
object val x = 1 val x_plus_1 = x + 1 end;; Characters 36-37: val x_plus_1 = x + 1 ^ Error: The instance variable x cannot be accessed from the definition of another instance variable
技术上的原因是因为当计算字段值时对象并不存在,所以引用这个对象或它的字段 或方法是一个错误。
要解决这个问题,对象可以包含一个初始化器(initializer),写为初始化表达 式。初始化表达式在对象创建后,但使用之前进行求值。对象在初始化时存在。 所以可以合法地引用它的字段或方法。
object val x = 1 val mutable x_plus_1 = 0 initializer x_plus_1 <- x + 1 end;;
初始化器常用来管理不变式,或者一个字段值依赖于其它字段。一个更真实点 的例子,写一个polygon对象,可以直接进行转换(通过副作用):
let new_imp_poly vertices = object val mutable vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform matrix = {< >}#transform_in_place matrix method transform_in_place matrix = vertices <- Array.map matrix#transform vertices end;;
这个对象潜在的效率问题是顶点表示为浮点数坐标,但是绘制使用整数坐标。 draw 方法在每次绘制时都要转换。
另一种方法是维护两个版本的顶点, float_vertices 和 int_vertices ,使用不变式 int_vertices = Array.map int_coord float_vertices 。
let new_imp_poly vertices = object (self : 'self) val mutable float_vertics = [||] val mutable int_vertices = [||] method draw = Graphics.fill_poly int_vertices method transform matrix = {< >}#transform_in_place matrix method transform_in_place matrix = self#set_vertices (Array.map matrix#transform vertices) method private set_vertices vertices = float_vertics <- vertices; int_vertices <- Array.map int_coord float_vertics initializer self#set_vertices vertices end;; val new_imp_poly : (float * float) array -> < draw : unit; transform : (< transform : float * float -> float * float; .. > as 'a) -> unit; transform_in_place : 'a -> unit > = <fun>
私有方法不会出现在对象类型中。
2.8 对象类型,强制转换(coercions),和子类化(subtyping)
我们创建的对象的类型已经相当复杂了。为了完全理解它,让我们进行一些 类型定义。我们并不关心给出最通用的多态类型,因此只用非多态的类 型。
type coord = float * float type transform = < transform : coord -> coord > type blob = < draw : unit; transform : transform -> blob > type collection = < add : blob -> unit; draw : unit; transform : transform -> collection >
注意 collection 类型与 blob 有两个地方不同。一个collection有一个额外 的 add 方法,并且 transform 方法返回另一个collection。
我们现在可以标注对象创建函数来获得简单类型。
let new_poly : coord array -> blob = fun vertices -> object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform matrix = {< vertices = Array.map matrix#transform vertices >} end;; val new_poly : coord array -> blob = <fun> ;; let new_circle : coord -> float -> blob = fun center radius -> object val center = center val radius = radius method draw = let x, y = int_coord center in Graphics.fill_circle x y (int_of_float radius) method transform matrix = (* 半径的transform 需要修改,这里使用固定值5. *) let r = 5. in (* let (r, _) = matrix#transform (radius, radius) in *) let (x, y) as center = matrix#transform center in Printf.printf "center:%f, %f. radius:%f\n" x y r; {< center = center; radius = r >} end;; val new_circle : coord -> float -> blob = <fun> ;; let new_collection : unit -> collection = fun () -> object val mutable items = [] method add item = items <- item :: items method draw = print_endline "draws..."; List.iter (fun item -> item#draw) items method transform matrix = {< items = List.map (fun item -> item#transform matrix) items >} end;; val new_collection : unit -> collection = <fun> ;;
现在类型变简单了,我们遇到了新的问题:真实的对象类型与希望的实际类型不 匹配。例如 transform 希望一个只含有一个方法的对象,但是真实的对象 有更多的方法。
# let circle = new_circle (0.0, 0.0) 0.1;; val circle : blob = <obj> # circle#transform (transform#new_translate 100.0 100.0);; Characters 17-54: circle#transform (transform#new_translate 100.0 100.0);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: This expression has type < multiply : < representation : float * float * float * float * float * float; .. > -> 'a; new_rotate : float -> 'a; new_scale : float -> float -> 'a; new_translate : float -> float -> 'a; representation : float * float * float * float * float * float; transform : float * float -> float * float > as 'a but an expression was expected of type transform The second object type has no method multiply
从理论上来说,传递的对象比期望的对象方法更多没有什么问题–多余的方法可以简单 的忽略。
2.8.1 强制转换
在OCaml中,这样的强制转换是合法的,但不是自动进行的。这与OCaml的方 针相符:所有的强制转换必须是显式的;例如,整数转换为浮点数必须通过 float_of_int 函数进行显式转换。
显式的对象强制转换有两种写法,"单强制转换"或者"双强制转换":
(object :> object-type) 单强制转换
(object : object-type :> object-type) 双强制转换
单强制转换表达式 (e :> t) 强制转换对象 e 为类型 t (如果合法 的话)。双强制转换表达式 (e : t1 :> t2) 表示先认为 e 的类型是 t1 ,然后强制转换到一个对象类型 t2 。大多数情况下,单强制转换 足够了。
circle#transform (transform#new_translate 100.0 100.0 :> transform);; - : blob = <obj>
另一个示例:
let matrix1 = (transform#new_translate 100.0 80.0) ** (transform#new_scale 100.0 100.0) let star = let poly = new_poly [| (0.0, 0.2); (0.1, 0.5); (0.0, 1.0); (-0.1, 0.5) |] in let star = new_collection () in star#add ((new_circle (0.0, 0.0) 0.1)#transform (matrix1 :> transform)); for i = 0 to 9 do let trans = transform#new_rotate (0.628 *. (float_of_int i)) in star#add (poly#transform (trans :> transform)) done; star;; ;; val star : collection = <obj> ;; # let image = new_collection ();; val image : collection = <obj> # image#add (new_circle (0.0, 0.0) 0.1);; - : unit = () # image#add star;; Characters 10-14: image#add star;; ^^^^ Error: This expression has type collection but an expression was expected of type blob The second object type has no method add # image#add (star :> blob);; Characters 10-24: image#add (star :> blob);; ^^^^^^^^^^^^^^ Warning 18: this ground coercion is not principal. - : unit = () # image#add (star : collection :> blob);; - : unit = ()
为什么单强制转换有时可以工作,但有时需要双强制转换呢?完整的技术说 明比较复杂,并且需要知道类型推导使用的算法细节。简单的规则是:如果 编译器编译一个单强制转换,将尝试使用一个双强制转换替换它。
在内部,编译器只使用双强制转换。当编译器遇到一个单强制转换 (e :> t2) ,它将使用推导出的一般预期类型(general expected type) t1构造 一个双强制转换 (e : t1 :> t2) 。然而,如果没有一个唯一的一般预期 类型就会导致错误。一般的指导规则如下:
一个单强制转换 (e :> t2) 可能产生错误的原因:
- 类型t2是递归的
- 类型t2是多态结构
如果任一条件成立,则使用双强制转换 (e : t1 :> t2) 。
在我们的示例中,强制转换 (star :> blob) 会产生警告(OCaml 4.00.1 版本),因为blob类型是递归的。编译器并不考虑表达式的真实类型,所以 尽管我们知道star是collection类型,仍然要使用双强制转换 (star : collection :> blob) ,在新版中,错误变成了警告。
2.8.2 子类化
这当然不是故事的全部,因为并不是所有的强制转换 (e : t1 :> t2) 都 合法。有两个必要条件:首先,表达式e必须拥有类型t1;第二,类型t1必须 是t2的子类型。
类型t1是t2的子类型,写作 t1 <: t2, 类型为t1的值可以用于需要一个 类型为t2的值的地方。
考虑如下类型定义:一个动物能吃,一条狗还会叫。
type animal = < eat : unit > type dog = < eat : unit; bark : unit >
子类化关系 dog <: animal 有效是因为一个dog对象拥有一个animal对象的 所有方法并且类型相同,因此一个dog对象e可以用在任何需要一个animal对 象的地方(因此 (e : dog :> animal) 是合法的)。
- 宽度和深度子类化
对象类型的子类化有两种形式,分别叫做宽度和深度子类化。宽度子类化的意 思是如果一个对象类型t1实现了一个对象类型t2的所有方法并且有相同的 类型,t1就是t2的子类型。一个对象类型中方法的顺序是随意的。深度子类化的意思是一个对象类型t1和一个对象类型t2有相同的方法,但 是t1中的方法类型是t2中对应类型的子类型,则t1是t2的子类型。
一般来说,方法类型可以为元组,列表,记录,函数,其它对象等包含不 同的类型构造器。每个类型构造器在OCaml中有它自己的子类化规则来描述 这个类型构造器同它的分量类型的不同关系。这些差异可以是协变的 (covariant),表示构造器之间的不同和一个分量类型的不同一样;逆变的 (contravariant),表示构造器之间的不同与一个分量类型的不同相反;或 是不变的(invariant),表示不完全是协变也不完全是逆变。
- 函数子类化
OCaml中几乎全部的类型构造器都是协变的,但是有两个例外。一个是指定 可变值的类型总是不变的。另一个例外比较有意思,用于函数类型。一个 函数类型 t1 -> t2 在类型t2的范围中是协变的,但是在域类型t1中是 逆变的。函数类型中的逆变起源于设计面向对象语言中的许多问题,理解起来比较 困难。考虑下面的 feed 函数喂一个动物:
# let feed (x : animal) = x#eat;; val feed : animal -> unit = <fun>
调用这个函数时,我们可以传递一个animal对象或一个dog对象–两个都支 持 eat 方法。因此,如果我们喜欢,可以强制转换函数类型为 dog -> unit :
# let feed_dog = (feed :> dog -> unit);; val feed_dog : dog -> unit = <fun>
现在考虑一个dog的bark函数:
# let do_bark (x : dog) = x#bark;; val do_bark : dog -> unit = <fun>
我们不能传递一个animal对象给 do_bark, 因为动物一般不会叫。 通常,我们不能使用一个函数类型 dog -> unit 替换一个函数类型 animal -> unit .
# (do_bark : dog -> unit :> animal -> unit);; Characters 0-41: (do_bark : dog -> unit :> animal -> unit);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type dog -> unit is not a subtype of animal -> unit Type animal = < eat : unit > is not a subtype of dog = < bark : unit; eat : unit >
这里,函数类型的参数类型强制转换和一般的强制转换方向是相反的,所以 是逆变。
- 递归对象类型的子类化
这种情况下,子类化规则是循环的:首先,假定子类化关系有效,然后证明 它有效。例如,考虑证明一个collection是一个blob. 类型是递归的,所以我们 先假定这个关系是有效的,然后证明它。下面是证明过程:
- 假设: collection <: blob
- 显示:
< add : blob -> unit; < draw : unit; draw : unit; transform : transform -> blob transform : transform -> collection > >
- 因为宽度子类化,我们可以忽略 add 方法.
< draw : unit; < draw : unit; transform : transform -> collection transform : transform -> blob > >
- draw 方法拥有同样的类型。 transform 方法调整为深度子类化。 显示如下.
transform -> collection <: trnasform -> blob
- 函数在范围类型上是协变的。最终的目标如下。
collection <: blob
这与假设相同。
2.9 收窄转换(narrowing)
在面向对象编程中,收窄转换是强制转换一个对象为它的子类型之一的能力——与 一般的强制转换相反。这常在一个对象的真实类型在程序的其它地方进行强 制转换后丢失了的情况下使用。例如,我们为猫和狗定义了一些类型。如果 我们想要一个包含狗和猫的列表,列表的元素必须强制转换到一个通用类型, 在这里为动物.
type animal = < eat : unit > type dog = < eat : unit; bark : unit > type cat = < eat : unit; meow : unit > ;; let fido : dog = ... let daphne : cat = ... let animals = [(fido :> animal); (daphne :> animal)]
因为强制转换, bark 和 meow 方法丢失了,这可能成为一个潜在的问 题。
在OCaml中收窄转换是不允许的。首先,我们论证为什么收窄转换是不好的, 然后讲解如何实现它。
2.9.1 为什么收窄转换不好
最常见的说法是收窄转换是不稳定的。实际上,更准确地说是因为不知道是 否有一个有用的,一般的收窄转换的形式与OCaml的类型系统兼容。
除去这个,好的设计原则也反对收窄转换。面向对象编程的一个重要优点是 实现的责任从客户端转移到了对象上。
观察我们的示例,问题在于一个概念,让我们叫它"speak",它命名为两种 不同的方式, bark 和 meow .更好的实现是在两种情况中使用相同的 名字,假定保留各自特别的名字。
type animal = < eat : unit; speak : unit > type dog = < eat : unit; speak : unit; bark : unit > type cat = < eat : unit; speak : unit; meow : unit > type lizard = < eat : unit; speak : unit; sleep : unit > let fido : dog = object (self) method speak = self#bark ... end let daphne : cat = object (self) method speak = self#meow ... end let fred : lizard = object (self) method speak = () ... end let animals = [(fido :> animal); (daphne :> animal); (fred :> animal)] let chorus (animals : animal list) = List.iter (fun animal -> animal#speak) animals
其它情况下,必须修改抽象来避免有损的强制转换。例如,某人这样决定, 在一个动物的集合里面,狗可以叫,但所有其它动物必须保持安静。解决方 法是避免第一步的强制转换。如果需要知道那些动物是狗,哪些是猫,就不 能使用一个单独的动物列表。可以使用多个列表代替。
2.9.2 实现收窄转换
尽管有这些论证,你仍然希望使用收窄转换,实现起来很简单。我们需要两 个东西:一个运行时的"标签"来指示对象的真实类型,和一个类型转换器分 析这些标签。标签必须是开放的才可以添加子类,我们有两种选择:多态变 体或异常。我们使用异常来表示,因为类型更容易写。主要的想法就是定义 一个 actual 方法返回真实的对象.
type narrowable_object = < actual : exn > type animal = < actual : exn; eat : unit > type dog = < actual : exn; eat : unit; bark : unit > type cat = < actual : exn; eat : unit; meow : unit > exception Dog of dog exception Cat of cat let fido : dog = object (self) method actual = Dog self ... end let daphne : cat = object (self) method actual = Cat self ... end let animals = [(fido :> animal); (daphne :> animal)] let chorus (animals : animal list) = List.iter (fun animal -> match animal#actual with | Dog dog -> dog#bark | Cat cat -> cat#meow | _ -> ()) anials
2.10 对象的替代
我们在本章看到的对象是简单的,用来封装一些数据和操作这些数据的函 数。在许多地方,这与抽象数据类型相似。事实上,我们本章所做的大部分 都可以使用模块系统完成。然而,有两个关键的不同点:1)在对象中,数据 的类型是完全隐藏的,2)对象是一等公民(first-class values),模块不是。 例如,一个模块实现一个多边形可以这样写.
module type PolySig = sig type poly val create : (float * float) array -> poly val draw : poly -> unit val transform : poly -> transform -> poly end;; module Poly : PolySig = struct type poly = (float * float) array let create vertices = vertices let draw vertices = Graphics.fill_poly (Array.map int_coord vertices) let transform vertices matrix = Array.map matrix#transform vertices end ;;
这种方法的主要问题是,尽管Poly.poly数据类型是抽象的。一个多边形的类 型是Poly.poly;一个圆的类型是Circle.circle。不使用其它方法就不能创建 一个同时包含多边形和圆形的列表。
一个简单的对象也可以表示为一个包含方法的记录。
type blob = { draw : unit -> unit; transform : transform -> blob } let rec new_poly vertices = { draw = (fun () -> Graphics.fill_poly (Array.map int_coord vertices)); transform = (fun matrix -> new_poly (Array.map matrix#transform vertices)) }
我们可以从记录和语言的其它部分构造许多对象的特性,但是我们在本章看 到的简单对象提供了一个简单,有用的编程模型。我们仍然忽略了最重要的 一个特性,继承。
3 类和继承
我们看到简单对象提供了抽象,但是提供了较少的软件重用方法,它是面向对象 编程的一个主要优点. 准确地说面向对象编程是什么?Mitchell提出了以下4条 基本属性.
- 抽象:实现细节在对象中隐藏;接口只是一组可公开访问的方法.
- 子类化:如果一个对象a拥有一个对象b的所有功能,我们就可以在任何希望b 的地方使用a.
- 动态查找:当一个消息发送给一个对象,要执行的方法由这个对象实现,并不 是靠程序的静态属性.也就是说,不同的对象可以对同一消息作出不同的响应.
- 继承:一类对象的定义可以重用来产生新一类的对象.
我们已经看到了前3个特性;现在来看看继承.在OCaml中,和许多其它语言一样, 继承出现在类中,一个类是描述了如何构造一个对象的模板.继承就是通过对一 个现有的类添加,删除,或修改方法和字段来创造一个新类的能力.
3.1 类基础
最简单的类定义看起来像定义一个对象,但是使用 class 关键字代替 let .类并不是OCaml中的对象,一个类定义也不是一个表达式.所有的类定 义必须出现在top level中.
class poly = object val vertices = [| (46, 70); (54, 70); (60, 150); (40, 150) |] method draw = Graphics.fill_poly vertices end;; class poly : object val vertices : (int * int) array method draw : unit end
从一个类创建一个对象,使用 new 关键字和类名.
# let p = new poly;; val p : poly = <obj> # p#draw;; - : unit = ()
3.1.1 类类型 (class types)
首相,我们定义了一个叫做 poly 的类,包含一个 vertices 字段和一个 draw 方法.poly类有一个类类型指定了它的方法和字段.
即使你熟悉面向对象编程,也可能没有见过类类型.然而,类类型很自然地出现 在同时包含类和模块的语言中.在OCaml中,模块中的所有定义必须有一个类型.一 个类不是一个类型(因为它包含代码),所以它必须有一个类型. 考虑上一章的 blobs模块.
module type BlobsSig = sig class poly : object val vertices : (int * int) array method draw : unit end val p : poly end module Blobs : BlobsSig = struct class poly = object val vertices = [||] method draw = Graphics.fill_poly vertices end let p = new poly end;;
另一个要注意的地方是 p 的类型 val p : poly .在这个上下文中,类名 poly可以代替多边形对象–也就是 type poly = < draw : unit > .一般 来说,当一个类名出现在一个类型表达式的上下文中,它就用来表示一个对象 类型. 类名并没有什么特殊的.两个拥有同样类型的方法的类表示同样的对象 类型,如下示例.
# class gunfighter = object method draw = print_string "Bang!\n" end;; class gunfighter : object method draw : unit end # let p : gunfighter = new poly;; val p : gunfighter = <obj> # p#draw;; - : unit = ()
3.1.2 参数化的类
当前的poly类不是很有用,因为顶点是固定的.如果我们希望更多的多边形,我 们可以定义一个参数化的类.参数通过 new 创建对象时传递.
class poly vertices = object val vertices = vertices method draw = Graphics.fill_poly vertices end;; class poly : (int * int) array -> object val vertices : (int * int) array method draw : unit end ;; # let p1 = new poly [| (46, 70); (54, 70); (60, 150); (40, 150) |];; val p1 : poly = <obj> # let p2 = new poly [| (40, 40); (60, 40); (60, 60); (40, 60) |];; val p2 : poly = <obj> # p1#draw; p2#draw;; - : unit = ()
在OCaml中,没有一个叫做对象构造器的语言特性.作为代替,类定义提供了它 的单独构造器,并且只有一种从类构造对象的方法–使用 new .
在一个类定义中,可以使用任何类表达式.例如,以下定义指定类 rectangle 是 poly 的一个特例.
3.1.3 类与let表达式
类定义中可以有前导的let定义,它在一个新对象创建前求值.let定义与一般 形式相同.示例如下.
class regular_poly n radius = let () = assert (n > 2) in let vertices = Array.create n (0, 0) in let step = 6.25 /. float_of_int n in let () = for i = 0 to n - 1 do let theta = float_of_int i *. step in let x = int_of_float (cos theta *. radius) in let y = int_of_float (sin theta *. radius) in vertices.(i) <- (x + 100, y + 100) done in object method draw = Graphics.fill_poly vertices end;; # let p = new regular_poly 7 100.0;; val p : regular_poly = <obj> # p#draw;; - : unit = ()
语法上,每个前导表达式必须是一个let定义,所以任何有副作用的计算操作 必须使用一个伪let形式 let () = … in .断言保证多边形至少有3个边.
3.1.4 类型推导
当你自己试图定义类时,就会碰到一个问题,OCaml推导出的类类型"太多态"了. 类可以多态.但必须显式的.考虑如下类型定义
class cell x = object method get =x end;; Characters 6-41: ......cell x = object method get =x end.. Error: Some type variables are unbound in this type: class cell : 'a -> object method get : 'a end The method get has type 'a where 'a is unbound
问题在于参数x是多态类型,所以这个类也有一个多态类型.如果你需要一个 多态类 1)必须在类名前的方括号中写出类型变量,2)你需要读完多态类那一 章
class ['a] cell (x : 'a) = object method get = x end;; class ['a] cell : 'a -> object method get : 'a end
大多数情况下,多态类是错误的.例如我们从前面一章的多边形对象复制定 义:
class poly vertices = object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform matrix = {< vertices = Array.map matrix#transform vertices >} end;; Characters 6-206: ......poly vertices = object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform matrix = {< vertices = Array.map matrix#transform vertices >} end.. Error: Some type variables are unbound in this type: class poly : (float * float) array -> object ('a) val vertices : (float * float) array method draw : unit method transform : < transform : float * float -> float * float; .. > -> 'a end The method transform has type (< transform : float * float -> float * float; .. > as 'b) -> 'a where 'b is unbound
类定义被拒绝是因为 transform 方法接受接受一个matrix, 它有 一个开放的,因此是多态的方法类型 .我们并不是真的想写一个多态类,只是 这个类型推导出来是多态的.有两个简单的解决方案:约束这个类型为非多态 的,或者使用一个多态的方法类型. 使用非多态约束这个类型:
type coord = float * float;; class poly vertices = object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform (matrix : < transform : coord -> coord >) = {< vertices = Array.map matrix#transform vertices >} end;; class poly : coord array -> object ('a) val vertices : coord array method draw : unit method transform : < transform : coord -> coord > -> 'a end
使用一个多态方法类型也很简单:
class poly vertices = object val vertices = vertices method draw = Graphics.fill_poly (Array.map int_coord vertices) method transform : 'a. (< transform : coord -> coord; ..> as 'a) -> 'self = fun matrix -> {< vertices = Array.map matrix#transform vertices >} end;; class poly : coord array -> object ('a) val vertices : coord array method draw : unit method transform : < transform : coord -> coord; .. > -> 'a end
前面的'a.是一个类型限定符。它指示这个类型变量明确地属于 transform 方法,不是这个类。
3.2 继承
一般来说,继承就是重用现有的类来定义新类的能力。一般情况下,通过 对一个现有的类添加方法和字段,或者修改它的方法实现,或者两者都有来 创建一个新类。当一个类B从一个类A继承,我们就说B是A的子类(subclass), A是B的父类(superclass)。当B也定义了一个A的子类型时(因此一个B类的对 象可以用于任何需要A类对象的地方),我们就说这个关系是"是一个(is-a)" 的关系。is-a关系是继承最常见的形式。
具体来说,一个类使用指令 inherit class-expression 从另一个类继承, 这将包括整个 class-expression 类到当前类。为了演示,我们构造一个 简单的动物王国模型。每个动物(animal)都会吃;一个宠物(pet)是一个拥有 主人(owner)和名字(name)的动物;一个宠物狗是会叫(bark)的宠物。
# class animal species = object method eat = Printf.printf "A %s eats,\n" species end;; class animal : string -> object method eat : unit end # class pet ~species ~owner ~name = object inherit animal species method owner : string = owner method name : string = name end;; class pet : species:string -> owner:string -> name:string -> object method eat : unit method name : string method owner : string end # class pet_dog ~owner ~name = object inherit pet ~species:"dog" ~owner ~name method speak = Printf.printf "%s barks!\n" name end;; class pet_dog : owner:string -> name:string -> object method eat : unit method name : string method owner : string method speak : unit end # let clifford = new pet_dog ~name:"Clifford" ~owner:"emily";; val clifford : pet_dog = <obj> # clifford#speak;; Clifford barks! - : unit = () # clifford#eat;; A dog eats, - : unit = ()
再说一次,继承的结果和包含非常相似。
3.2.1 方法覆盖
前面的两个继承的例子都是包含父类的行为,但是没有修改它。子类也可以 通过重定义它的方法进行修改。例如,宠物有一个名字,我们希望当它吃的 时候用宠物的名字代替种类名字。我们可以重定义 eat 方法:
class pet ~species ~owner ~name = object inherit animal species method owner : string = owner method name : string = name method eat = Printf.printf "%s eats.\n" name end;;
一些狗会保护它们的食物。我们可以重定义 eat 方法
class pet_dog ~owner ~name = object (self : 'self) inherit pet ~species:"Dog" ~owner ~name as super method speak = Printf.printf "%s barks!\n" name method prepare_to_eat = Printf.printf "%s growls menacingly.\n" name method eat = self#prepare_to_eat; super#eat end;; # let clifford = new pet_dog ~owner:"Emily" ~name:"Clifford";; val clifford : pet_dog = <obj> # clifford#eat;; Clifford growls menacingly. Clifford eats. - : unit = ()
3.2.2 类类型
继承层次的设计会影响对一个系统编程的自由。对于有名无实类型的语言 确实是这样,比如C++或Java,子类型由层次进行约束。例如,在一个有名无 实类型的系统中,一个对象的类型对应于类名,所以一个 pet_dog 可以 强制转换为一个 pet ,也可以到一个 animal ,但是其它的强制转换是 不允许的。
这会变成过度限制。比如在设计过程中我们决定需要一个新类。以动物为 例,我们希望一个 farm_animal 类,动物有主人但也许没有名字,或者 一个 vocal_animal 类,可以发声,也许是野生的。一条狗可以是一个 pet ,是一个 farm_animal, 也可以是一个 vocal_animal 。
在最坏的情况下,继承层次由于许多特性集合的组合而变得非常复杂。带有 接口的语言,比如Java,允许一个类满足多个接口定义来试图解决这个问题。
interface farm_animal {void eat (); String owner();}; interface vocal_animal {void eat (); void speak();} class dog extends pet implements farm_animal, vocal_animal {...}
这种方法的缺点是这些接口必须在类型定义时指定,需要设计者预料什么 接口会有用。
在OCaml中,情况不同。继承层次约束了类的规格和实现,但是不影响子类 型。这里是我们如何扩展动物的例子来支持农场动物和发声动物的示例。
# class type farm_animal = object inherit animal method owner : string end;; class type farm_animal = object method eat : unit method owner : string end # class type vocal_animal = object inherit animal method speak : unit end;; class type vocal_animal = object method eat : unit method speak : unit end ;; # (clifford : pet_dog :> farm_animal);; - : farm_animal = <obj>
注意 inherit 子句,类类型不接受参数。在这个上下文中,名字 animal 用来表示类类型, inherit 指令仍然等同于文本包含。
结构化的子类型的主要优点是一个对象可以强制转换到它兼容的任意类型, 类型并不需要先前指定。当然也有不利,子类化太过自由;一些强制转换 在语义上说不通。
# class cat ~owner ~name = object inherit pet ~species:"cat" ~owner ~name method speak = Printf.printf "%s meows.\n" name end;; class cat : owner:string -> name:string -> object method eat : unit method name : string method owner : string method speak : unit end # let my_cat = (clifford :> cat);; val my_cat : cat = <obj> # my_cat#speak;; Clifford barks! - : unit = () (* ...这就是所谓的cosplay? 狗还是狗,不会meow *)
3.2.3 类类型–约束和隐藏
一个类的类型可以约束,语法如下,class-type是一个类类型:
class class-name parameter1 … patametern : class-type = class-expression
也可以把约束放到对象类型上,使用一个显式约束 'self = object-type ,或者使用等价的 object-type as 'self 。
class cat ~name ~owner = object (self : 'self) constraint 'self = < eat : unit; speak : unit; .. > inherit pet ~species:"cat" ~name ~owner method speak = Printf.printf "%s meows.\n" name end;;
作为一个程序员,你可能希望写一个类型约束来保证你的实现与你的接口 匹配。然而,还有一些使用类型约束的其它原因。和OCaml的其它部分不同, 一个类上的类型约束可以修改程序行为。一个约束可以做什么,不能做什 么:
- 一个约束不能用来隐藏公共方法
- 一个约束可以让一个私有方法变为公共的
- 一个约束可以用来隐藏字段和私有方法
约束公共方法和对象类型的约束相同。如果一个对象有一个公共方法,则这个方法 必须出现在类型中。
# (object method x = 1 method y = 2 end : < x : int >);; Characters 1-37: (object method x = 1 method y = 2 end : < x : int >);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: This expression has type < x : int; y : int > but an expression was expected of type < x : int > The second object type has no method y
第二个属性,让私有方法变成公共的,有一点惊讶。常用于一个父类定义了 一个私有方法,而一个子类想让它变成公共的。
# class foo = object (self : 'self) constraint 'self = < x : int; .. > method private x = 1 end;; class foo : object method x : int end
第三种类型的约束用来隐藏字段和私有方法,需要一些讨论。当一个类隐藏 一个字段,或一个私有方法,这个字段在子类中就不可访问,也意味着这个 字段或方法不能被覆盖。考虑如下类定义。
class a = object (self) method private f_private = print_string "aaaa\n" method test_a = self#f_private end;; class b = object (self) inherit a method private f_private = print_string "bbbb\n" method test_b = self#test_a; self#f_private end;; # (new b)#test_b;; bbbb bbbb - : unit = ()
f_private 方法是私有的,但是它在两个类中是相同的。类b中的 f_private 定义覆盖了类a中的定义,所以结果打印了bbbb两次。现在考 虑下使用类型约束隐藏 f_private 。
class type a_type = object method test_a : unit end;; class a : a_type = object (self) method private f_private = print_string "aaaa\n" method test_a = self#f_private end;; class b = object (self) inherit a method private f_private = print_string "bbbb\n" method test_b = self#test_a; self#f_private end;; # (new b)#test_b;; aaaa bbbb
这种情况下,结果不同。通过隐藏类a中的 f_private 方法,它不能在子 类b中被覆盖,因此程序的行为被修改了。
这个属性对于字段同样有效。当一个字段通过一个类型约束隐藏时,它就不能 在子类中访问了。子类中拥有同样名字的新字段定义就无关了。
3.2.4 类和类类型作为对象类型
我们已经说过了,一个类不是一个类型,但是在一个类型表达式的上下文中, 类名用来表示一个对象类型。这对于类类型同样有效–一个类类型是一个类 的类型,但是它在一个类型表达式的上下文中也用来表示一个对象的类型。 对象类型的形式是一个去掉字段类型的类类型。
从类名成型两种类型的对象类型:完全类型和开放类型. 类表达式 class-name 代表对象完全拥有类 class-name 的方法, 类表达式 #class-name 代表对象拥有这些方法,或许有更多的方法. 也就是说,一个 类型表达式 #class-name 代表一个开放对象类型.
和一个开放对象类型一样,一个类型表达式 #class-name 是多态的.
3.3 继承不是子类化
总结一下通过继承可以完成的操作:
- 添加新的字段和新的私有方法,
- 添加新的公共方法
- 覆盖字段或方法,但是不能修改类型.
字段和私有方法不能出现在对象类型中,并且方法覆盖不允许修改方法的类型.所 以从一个类型的观点来看,如果一个类B是一个A的子类,它可能比A拥有更多的 方法,但是除此之外没有修改.通过宽度子类化,这通常表示B的对象类型是A的 对象类型的一个子类型.事实上,在大多数语言中,子类化和子类型是相同的, 并且是互相共存的.
不幸的是,这个一致性只有在对象类型与'self协变的情况下成立;也就是说, 当这个对象没有二元方法的时候.考虑一个类类型 comparable 用于可 比较的对象.
type comparison = int class type comparable = object ('self) method compare : 'self -> comparison end;;
compare 是一个二元方法;它接受另一个对象类型'self并完成比较.实现的 常用技术是添加一个 representation 方法暴露内部实现,因此可以完成 比较.
class int_comparable (i : int) = object (self : 'self) method representation = i method compare (j : 'self) = i - j#representation end;; class string_comparable (s : string) = object (self : 'self) method representation = s method compare(s2 : 'self) = String.compare s s2#representation end;;
我们后面决定实现一些子类来输出,比较对象.
class int_print_comparable i = object (_ : 'self) inherit int_comparable i method print = print_int i end;; class string_print_comparable s = object (_ : 'self) inherit string_comparable s method print = print_string s end;;
我们可能希望一个可输出和可比较的整数也是一个可比较的整数.但是,期 望的类型转换时错误的.
# (new int_comparable 1 :> comparable);; Characters 0-36: (new int_comparable 1 :> comparable);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type int_comparable = < compare : int_comparable -> int; representation : int > is not a subtype of comparable = < compare : comparable -> comparison > Type comparable = < compare : comparable -> comparison > is not a subtype of int_comparable = < compare : int_comparable -> int; representation : int > # (new int_print_comparable 1 :> int_comparable);; Characters 0-46: (new int_print_comparable 1 :> int_comparable);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type int_print_comparable = < compare : int_print_comparable -> int; print : unit; representation : int > is not a subtype of int_comparable = < compare : int_comparable -> int; representation : int > Type int_comparable = < compare : int_comparable -> int; representation : int > is not a subtype of int_print_comparable = < compare : int_print_comparable -> int; print : unit; representation : int >
实际的问题在于 compare 方法是'self类型的逆变,因为它接受一个类型 为'self的值作为参数. 这与正常情况相反.这是对应于类和类类型的对象类 型.
comparable = < compare : 'self -> bool > as 'self int_comparable = < representation : int; compare : 'self -> bool > as 'self int_print_comparable = < representation : int; compare : 'self -> bool; print : unit > as 'self
让我们尝试证明 int_comparable <: comparable 的子类化关系来看看哪 里错了.各自的'self对应于类型名,因此我们尝试如下证明:
< representation : int; compare : int_comparable -> bool > <: < compare : comparable -> bool >
试图证明:
- 类型是递归的,所以我们先假定子类化是有效的: int_comparable <: comparable
- 通过宽度子类化,可以丢弃 representation 方法.如下:
< compare : int_comparable -> bool > <: < compare : comparable -> bool >
- 通过深度子类化,如下:
(int_comparable -> bool) <: (comparable -> bool)
- 通过函数子类化,如下:
comparable <: int_comparable
- 这与最初的假定相反,因此证明失败.
当然,这并不表示我们不能写一个可比较对象的通用函数,它只是意味着这个 函数需要一个开放类型 #comparable,不是严格的非多态类型 comparable .
# let sort (l : #comparable list) = List.sort (fun e1 e2 -> e1#compare e2) l;; val sort : (#comparable as 'a) list -> 'a list = <fun> # let l = List.map (new int_print_comparable) [9; 1; 6; 4];; val l : int_print_comparable list = [<obj>; <obj>; <obj>; <obj>] # List.iter (fun i -> i#print) (sort l);; 1469- : unit = ()
我们可能会同意严格类型 comparable 没有有用的对象,但是类型 int_print_comparable 和 int_comparable 的关系怎么样?如果看起来 需要保持子类化关系,解决方法是避免使用二元方法. compare 方法并不是 真的需要一个'self类型的参数,它只是需要一个有同样表示的对象.这个类可 以通过一个约束的参数类型重新实现.
class int_comparable (i : int) = object method representation = i method compare : 'a. (< representation : int; .. > as 'a) -> comparison = (fun j -> Pervasives.compare i j#representation) end;; class int_print_comparable i = object inherit int_comparable i method print = print_int i end;;
它保证了 int_print_comparable 是 int_comparable 的一个子类型,但 是这有几点不利. 一是新对象不再拥有类型#comparable,所以不能写真正的 通用函数.另一个问题是类型有点复杂.然而,通过一些努力,我们获得了想要 的结果.
# let compare_int (e1 : #int_comparable) (e2 : #int_comparable) = e1#compare e2;; val compare_int : #int_comparable -> #int_comparable -> comparison = <fun> # let sort_int (l : #int_comparable list) = List.sort compare_int l;; val sort_int : (#int_comparable as 'a) list -> 'a list = <fun> # let l = List.map (new int_print_comparable) [9; 1; 3; 2];; val l : int_print_comparable list = [<obj>; <obj>; <obj>; <obj>] # List.iter (fun i -> i#print) (sort_int l);; 1239- : unit = ()
3.4 模块和类
OCaml包含了两个进行模块化和抽象的主要系统:模块系统和对象系统.在许多 方面,这两个系统很相似.它们都提供了抽象和封装的机制,子类化(在对象中 忽略方法,在模块中忽略字段),和继承(对象使用 inherit; 模块使用 include ).然而,两个系统是不能比较的.一方面,对象有一个优势:对象是 一等公民,模块不是–也就是说,模块不能进行动态查找.另一方面,模块有一 个优势:它可以包含类型定义,对象不能.
我们已经建议采用模块来手动隐藏二元方法定义中的内部表示.举个例子,让 我们使用模块来隐藏一个可比较对象的表示.
module type IntComparableSig = sig type rep class int_comparable : int -> object ('self) method representation : rep method compare : 'self -> comparison end end module IntComparable : IntComparableSig = struct type rep = int class int_comparable (i : int) = object (_ : 'self) method representation = i method compare (j : 'self) = Pervasives.compare i j#representation end end
使用这种方法来隐藏表示也有一个副作用,必须通过类int_comparable才 能构造一个对象类型int_comparable.我们可以让它变成优点.在动物的示例 中,如果我们希望保证狗不能转换为猫,可以为猫提供一个证书来证明它们的 真实性.
module type CatSig = sig type cert class cat : owner:string -> name:string -> object inherit pet method cert : cert method speak : unit end end module Cat : CatSig = struct type cert = unit class cat ~owner ~name = object inherit pet ~species:"cat" ~owner ~name method cert = () method speak = Printf.printf "%s meows.\n" name end end
不用管证书是什么,因为它是抽象的。当然,这个技术主要用来防止意外的 强制转换。如果一条狗真的希望变成一只猫,它可以从它们那里获得一个证 书。
函子在类构造器中也很有用。例如,有许多种类的可比较对象,整数,浮点 数,字符串,可比较对象的元组等。我们需要的就是构造一个函数比较这些 值。通用类如下定义。
(* 小于:负数; 等于:0; 大于:正数 *) type comparison = int class type comparable = object ('self) method compare : 'self -> comparison end;; module type CompareSig = sig type t val compare : t -> t -> comparison end;; module type ComparableSig = sig type t type rep class element : t -> object ('self) method representation : rep method compare : 'self -> comparison end end;; module MakeComparable (Compare : CompareSig) : ComparableSig with type t = Compare.t = struct type t = Compare.t type rep = Compare.t class element (x : t) = object (self : 'self) method representation = x method compare (item : 'self) = Compare.compare x item#representation end end;; module IntComp = MakeComparable ( struct type t = int let compare a (b:int) = Pervasives.compare a b end);; module IntComp : sig type t = int type rep class element : t -> object ('a) method compare : 'a -> comparison method representation : rep end end ;; let i1 = new IntComp.element 3;; val i1 : IntComp.element = <obj> # i1#compare (new IntComp.element 5);; - : comparison = -1
表示类型rep是抽象的,但是元素值的类型t不是。
3.5 多态方法和虚类
一个聚集(collection),一般来说,就像一组元素的集合。有许多种类的聚 集,一些基于数组或列表的实现,另一些基于访问模式,比如栈和队列。
元素是什么并不重要,因此我们假定元素可以显示。使用多态集合也许会很有 用,但我们先不用它,后面再讲。
class type element = object method print : unit end class type collection = object method length : int end class type enumerable_collection = object inherit collection method iter : (element -> unit) -> unit method fold : ('a -> element -> 'a) -> 'a -> 'a end;;
3.5.1 多态方法
下一步,list和array是两种具体类型的聚集,主要是它们的访问方式不同; 一个list是一个栈,然而array允许随机访问。当我们试图提供一个具体的 list实现时,遇到一个错误,因为 fold 方法是多态的。
class broken_list_collection = object val elements = [] method fold f x = List.fold_left f x elements end;; Error: Some type variables are unbound in this type: class broken_list_collection : object val elements : 'a list method fold : ('b -> 'a -> 'b) -> 'b -> 'b end The method fold has type ('b -> 'a -> 'b) -> 'b -> 'b where 'a is unbound
问题在于OCaml假定类必须是多态的,不是方法(这是我们需要的)。OCaml允 许方法多态,但是必须使用下面的语法进行显式标注:
method identifier : 'type-variable … 'type-variable. type = expression
类型表达式'type-variable … 'type-variable. type 叫做统一限定类 型,并且 . 前面的类型变量绑定到指定的方法的类型上,并不是整 个类。注意这个类型约束需要出现在方法名的后面,因此函数要显式编写。 下面是正确的定义.
class list_collection = object val mutable elements : element list = [] method length = List.length elements method add x = elements <- x :: elements method remove = elements <- List.tl elements method iter f = List.iter f elements method fold : 'a. ('a -> element -> 'a) -> 'a -> 'a = (fun f x -> List.fold_left f x elements) end;; class list_collection : object val mutable elements : element list method add : element -> unit method fold : ('a -> element -> 'a) -> 'a -> 'a method iter : (element -> unit) -> unit method length : int method remove : unit end ;; class array_collection size init = object val elements = Array.create size init method length = size method set i x = elements.(i) <- x method get i = elements.(i) method iter f = Array.iter f elements method fold : 'a. ('a -> element -> 'a) -> 'a -> 'a = fun f x -> let rec loop i x = if i = size then x else loop (i + 1) (f x elements.(i)) in loop 0 x end;;
3.5.2 (Virtual/abstract)抽象类和方法
在这里,我们有了两个类类型 collection 和 enumerable_collection; 两个具体类 list_collection 和 array_collection .使用类类型的原 因是因为collection和enumerable_collection没有具体的实例;只有子类有 具体实例.
现在假如我们希望可以打印一个聚集.我们可以在具体类 list 和 array 中实现一个 print 方法,但事实上这个实现一模一样.
method print = self#iter (fun element -> element#print)
另一种实现它的方法是把实现"提升"到父类enumerable_collection中.问题 在于enumerable_collection是一个类类型,不是一个类,因此不能包含代码.
解决方法是把enumerable_collection定义为一个虚类(virtual class), 这是一个有部分或全部方法被省略的类.实现被忽略的方法叫做虚方法 (virtual method),如下定义:
method virtual identifier : type
任何包含虚方法的的类都必须定义为虚的,使用 class virtual 语法.为 了示例的完整性,我们也把collection定义为虚的.
class virtual collection = object method virtual length : int end;; class virtual enumerable_collection = object (self : 'self) inherit collection method virtual iter : (element -> unit) -> unit method virtual fold : 'a. ('a -> element -> 'a) -> 'a -> 'a method print = self#iter (fun element -> element#print) end;;
虚类不能被实例化.一旦所有方法都被实现,一个类的虚状态就可以移除了.
class list_collection = object inherit enumerable_collection val mutable elements : element list = [] method length = List.length elements method add x = elements <- x :: elements method head = List.hd elements method remove = elements <- List.tl elements method iter f = List.iter f elements method fold : 'a. ('a -> element -> 'a) -> 'a -> 'a = (fun f x -> List.fold_left f x elements) end;;
3.5.3 术语
忽略实现的类在许多面向对象语言中是一个标准特性.主流术语中叫做抽象类 (abstract class);虚方法(virtual method)是使用动态查找决定的方法.
OCaml使用的非标准术语可能有点混乱,尤其是对于不熟悉OCaml的或者只是 学习这个语言的人.然而,有一个好的论点可以证明OCaml使用的是正确的术 语,主流术语是错误的.这里是从American-Heritage字典中选择的术语 "abstract"和"virtual"的定义.
- abstract … 4.思考或陈述不涉及一个特定的实例
- virtual 1.现存的或作为结果的本体或效果没有一个实际的事实,形式, 或名字…
更宽松地说,如果一些东西是抽象的(abstract),就表示它存在但是没有完全 指定或定义;如果一些东西是虚的(virtual),它看起来存在,但事实上并不 存在. 因为这个理由,virtual是更正确的术语,因为一个虚类,可以像一个普 通类一样使用–除了一点:不能被实例化,因为它不是完全存在的.
3.5.4 栈
返回我们的示例,考虑stack类.栈靠它们的行为定义:元素从栈顶压入,然后 从栈顶获得,按后进先出(LIFO)的顺序.通用的栈是一个包含2个虚方法的虚 类: push : element -> unit 往栈顶压入元素, pop : element 从栈 顶移除并获得一个元素.另外,我们还包括两个派生方法: dup: unit 复制 栈最上面的元素, swap : unit 交换最上面的两个元素.
class virtual stack = object (self : 'self) inherit collection method virtual push : element -> unit method virtual pop : element method dup = let x = self#pop in self#push x; self#push x method swap = let x1 = self#pop in let x2 = self#pop in self#push x1; self#push x2 end;;
让我们使用数组创建一个真实的子类 bounded_stack .
class bounded_stack size = let dummy = object method print = () end in object inherit stack val data = new array_collection size dummy val mutable index = 0 method push x = if index = size then raise (Failure "stack is full"); data#set index x; index <- index + 1 method pop = if index = 0 then raise (Failure "stack is empty"); index <- index - 1; data#get index method length = data#length end;;
3.5.5 列表与栈
bounded_stack类演示了两个关系:它是一个(is-a)栈,因此从stack类继承; 它有一个(has-a)array,它包含一个array字段用来实现一个stack需要的虚 方法.
另一种构造栈的方法是使用一个list,但是这种情况下栈和list非常相似,我 们可以说一个栈就是一个(is-a)list,并且从list_collection类继承.OCaml支 持多重继承.
class unbounded_stack = object (self : 'self) inherit list_collection inherit stack method push x = self#add x method pop = let x = self#head in self#remove; x end;; class unbounded_stack : object val mutable elements : element list method add : element -> unit method dup : unit method fold : ('a -> element -> 'a) -> 'a -> 'a method head : element method iter : (element -> unit) -> unit method length : int method pop : element method print : unit method push : element -> unit method remove : unit method swap : unit end
结果类拥有两个父类的方法和字段,所以除了是一个stack,它也是一个 enumerable_collection(还是一个list_collection).
这个构造正确么?它依赖于是否允许以一个列表来查看栈.例如 unbounded_stack类有两个方法来添加一个元素: push 和 add,并且它 们是相同的(push调用了add).如果它可以接受一个子类覆盖其中的一个方法,那么 这个多重继承就是可以接受的;否则就不是.
4 多重继承
多重继承是从多于一个父类进行继承的能力.
多重继承是面向对象编程最受争议的地方之一. 事实上,多重继承经常是组合 特性和抽象的一个最简单和最优雅的方法.然而,有两个主要问题:
- 遮蔽(shadowing):当两个父类定义一个相同名字的方法时会发生什么?
- 重复继承:当一个类被继承多次时会发生什么?
我们将看到,OCaml的继承模型很简单.它基本上等同于文本包含,并且遮蔽有一 个一般原则:如果一个方法定义了多次,最后的定义将胜出.
4.1 多重继承的示例
两种类型的组合:特性无关,特性相关.
4.1.1 从多个无关的类继承
从编程的观点看,从无关的类继承是很简单的.结果是所有部分的组合.
4.1.2 从多个虚类继承
有时从一个部分虚类继承是用来添加功能的机制.如下示例:
type comparison = int class virtual comparable = object (self : 'self) method virtual compare : 'self -> comparison method less_than (x : 'self) = compare self x < 0 end;; class virtual number = object (self : 'self) method virtual zero : 'self method virtual neg : 'self method virtual compare : 'self -> comparison method abs = if self#compare self#zero < 0 then self#neg else self end class float_number x = object (self : 'self) inherit comparable inherit number val number = x method representation = number method zero = {< number = 0.0 >} method neg = {< number = -. number >} method compare y = Pervasives.compare x y#representation end
4.1.3 混合(Mixins)
混合是一个类用来增加功能或修改一个子类的行为,但是并没有显式的is-a 关系.
4.2 覆盖和遮蔽
当一个对象定义一个名字两次会发生什么?先看看模块的情况.
# module M = struct let x = 1;; Printf.printf "x = %d!\n" x;; let x = 2;; end;; x = 1! module M : sig val x : int end # M.x;; - : int = 2
对象与模块有点不同,但是命名是相似的.考虑如下定义:
# let a = object method get = 1 method get = 2 end;; val a : < get : int > = <obj> # a#get;; - : int = 2
再来看看字段.
# let b = object val x = 1 method get = x val x = 2 end;; Characters 50-51: val x = 2 ^ Warning 13: the instance variable x is overridden. The behaviour changed in ocaml 3.10 (previous behaviour was hiding.) val b : < get : int > = <obj> # b#get;; - : int = 2
4.3 重复继承
重复继承发生在一个类在继承层次中顺着多个路径继承另一个类的情况.
在OCaml中, inherit 子句非常接近于文本包含–结果等同于使用类的文本替换 要继承的 inherit 子句.
对于任何给定的方法和字段,记住一个规则:最后的定义才管用.
class a = object method x = 1 end;; class b = object inherit a method x = 2 inherit a end;; # (new b)#x;; - : int = 1
菱形问题与此类似.
class person = object method name = "Francois" method address = "San Jose" end;; class programmer = object inherit person method lang = "OCaml" end;; class french_person = object inherit person method lang = "Francais" end;; class french_programmer = object inherit programmer inherit french_person end;;
文本展开后,并保持每个方法的最后定义,下面的类等同于french_programmer.
class french_programmer_flattened = object method name = "Francois" method lang = "Francais" method address = "San Jose" end
可见字段的覆盖和方法覆盖相同.然而,隐藏字段不能覆盖;它们被复制.
class type a_type = object method set : int -> unit method get : int end;; class a : a_type = object val mutable x = 0 method set y = x <- y method get = x end;; class b = object inherit a as super1 inherit a as super2 method test = super1#set 10; super2#get end;; # (new b)#test;; - : int = 0
4.4 避免重复继承
至少有两种重复继承的策略:覆盖和复制.没有哪个是最好的.
OCaml使用以下策略:可见字段和方法是用覆盖策略,隐藏字段和方法使用复制 策略.多重继承的语义可以简单地表示为,"它只是文本展开",但是问题在于可 能需要知道所有重复继承的文本.
4.4.1 Is-a vs. has-a
使用包含代替多重继承
4.4.2 Mixins revisited
避免重复继承的一个方法是尽可能延后使用多重继承.
5 多态类
到此为止,我们已经看了许多种的类和类类型定义.类可以是固定的,或者通过普 通值参数化.另外,类和类类型也可以是多态的,表示它们可以通过类型参数 化,和OCaml中的其它表达式和类型一样(除了模块类型).
这种通用的面向对象编程在其它语言中有不同的形式.Eiffel语言支持 genericity;C++有一个叫做模板的概念;Java有类型参数化的类叫做generics.
在OCaml中,对于类的多态并不是一个新概念.它与贯穿整个语言的概念相同,并 且对于类和类类型来说和其它构造的工作方法相同.
5.1 多态字典
一个map是一个包含键值对的字典,通过一个定义键的线性序的compare函数进 行参数化.简短起见,我们使用关联列表实现map.
type ordering = Smaller | Equal | Larger class ['key, 'value] map (compare : 'key -> 'key -> ordering) = let equal key1 (key2, _) = compare key1 key2 = Equal in object (self : 'self) val elements : ('key * 'value) list = [] method add key value = {< elements = (key, value) :: elements >} method find key = snd (List.find (equal key) elements) end;; class ['key, 'value] map : ('key -> 'key -> ordering) -> object ('a) val elements : ('key * 'value) list method add : 'key -> 'value -> 'a method find : 'key -> 'value end
这个类定义通过两个类型进行参数化,'key和'value,写在类名前面的方括号 中['key, 'value].
5.1.1 多态类中的自由变量
类定义中不允许自由类型变量,所有的类型变量必须作为一个参数,或者在类 定义的某处进行绑定.下面的定义是错误的.
class ['a] is_x x = object (self : 'self) method test y = (x = y) end;; Characters 6-71: ......['a] is_x x = object (self : 'self) method test y = (x = y) end.. Error: Some type variables are unbound in this type: class ['a] is_x : 'b -> object method test : 'b -> bool end The method test has type 'b -> bool where 'b is unbound
错误的原因在于参数x的类型没有指定为'a.
class ['a] is_x (x : 'a) = object (self : 'self) method test y = (x = y) end;; class ['a] is_x : 'a -> object method test : 'a -> bool end
5.1.2 实例化一个多态类
实例化一个类(用来获得一个对象),与非多态类相同.
# let compare_int (i : int) (j : int) = if i < j then Smaller else if i > j then Larger else Equal;; val compare_int : int -> int -> ordering = <fun> # let empty_int_map = new map compare_int;; val empty_int_map : (int, '_a) map = <obj> # let one = empty_int_map#add 1 "One";; val one : (int, string) map = <obj> # empty_int_map;; - : (int, string) map = <obj>
我们可以做出额外的一步来定义一个类来指定键的类型为整数.
class ['value] int_map = [int, 'value] map compare_int;; class ['value] int_map : [int, 'value] map
如果我们想要约束的更多的话:
class int_map2 = [int, string * float] map compare_int;; class int_map2 : [int, string * float] map
5.1.3 从多态类继承
除了多态父类的类型参数必须显式指定外,从一个多态类继承和一般情况下 相同.
class ['key, 'value] iter_map compare = object inherit ['key, 'value] map compare method iter f = List.iter (fun (key, value) -> f key value) elements end;; class ['key, 'value] iter_map : ('key -> 'key -> ordering) -> object ('a) val elements : ('key * 'value) list method add : 'key -> 'value -> 'a method find : 'key -> 'value method iter : ('key -> 'value -> unit) -> unit end ;; class ['key, 'value] map_map compare = object (self : 'self) inherit ['key, 'value] map compare method map f = {< elements = List.map (fun (key, value) -> key, f value) elements >} end;; class ['key, 'value] map_map : ('key -> 'key -> ordering) -> object ('a) val elements : ('key * 'value) list method add : 'key -> 'value -> 'a method find : 'key -> 'value method map : ('value -> 'value) -> 'a end
注意map方法的类型,它需要一个类型为'value -> 'value的函数参数.我们 可能希望一个更通用的类型.给定一个obj : ['key, 'value1] map_map的对 象和一个函数f : 'value1 -> 'value2,表达式obj#map f的类型为['key, 'value2] map_map.
使用更强限制性的类型有一个好原因.例如:
class ['key, 'value] dfault_map compare (default : 'value) = object (self : 'self) inherit ['key, 'value] map_map compare as super method find key = try super#find key with Not_found -> default end ;; class ['key, 'value] dfault_map : ('key -> 'key -> ordering) -> 'value -> object ('a) val elements : ('key * 'value) list method add : 'key -> 'value -> 'a method find : 'key -> 'value method map : ('value -> 'value) -> 'a end
当修改elements的类型后不修改default值的类型是不安全的.因此,方法map 更通用的类型是不安全的,因为它没有修改default的值.
唯一安全的方法就是对象类型是不变的.
5.2 多态类类型
多态类拥有多态类类型.
二叉搜索树
class type ['a] tree = object ('self) method add : 'a -> 'a tree method mem : 'a -> bool end;; class ['a] node (compare : 'a -> 'a -> ordering) (x : 'a) (l : 'a tree) (r : 'a tree) = object (self : 'self) val label = x val left = l val right = r method mem y = match compare y label with | Smaller -> left#mem y | larger -> right#mem y | Equal -> true method add y = match compare y label with | Smaller -> {< left = left#add y >} | Larger -> {< right = right#add y >} | Equal -> self end;; class ['a] leaf (compare : 'a -> 'a -> ordering) = object (self : 'self) method mem (_ : 'a) = false method add x = new node compare x (new leaf compare) (new leaf compare) end;;
这个实现很好,不过有一点效率问题,因为add方法每次创建整个新的叶子。 因为所有的叶子都是相同的,我们可以使用self代替。但是我们会遇到类型 错误,因为类型'self不等于'a tree。
class ['a] leaf (compare : 'a -> 'a -> ordering) = object (self : 'self) method mem (_ : 'a) = false method add x = new node compare x self self end;; Characters 139-143: method add x = new node compare x self self ^^^^ Error: This expression has type < add : 'a -> 'b; mem : 'a -> bool; .. > but an expression was expected of type 'a tree Self type cannot be unified with a closed object type
问题在于,类型'self是'a leaf的子类型,但是类node的参数为一个'a tree类型。一个解决方法是强制转换self到正确的类型。
class ['a] leaf (compare : 'a -> 'a -> ordering) = object (self : 'self) method mem (_ : 'a) = false method add x = new node compare x (self :> 'a tree) (self :> 'a tree) end;; class ['a] leaf : ('a -> 'a -> ordering) -> object method add : 'a -> 'a node method mem : 'a -> bool end
5.2.1 强制转换多态类
先定义一个简单例子
class ['a, 'b] mut_pair (x0 : 'a) (y0 : 'b) = object (self : 'self) val mutable x = x0 val mutable y = y0 method set_fst x' = x <- x' method set_snd y' = y <- y' method value = x, y end;;
使用动物的例子来测试它。
class virtual animal (name : string) = object (self : 'self) method eat = Printf.printf "%s eats.\n" name end;; class dog (name : string) = object inherit animal name method bark = Printf.printf "%s barks!\n" name end;; let dogs = new mut_pair (new dog "Spot") (new dog "Rover");; val dogs : (dog, dog) mut_pair = <obj> ;; let eat2 animals = let x, y = animals#value in x#eat; y#eat;; val eat2 : < value : < eat : 'a; .. > * < eat : 'b; .. >; .. > -> 'b = <fun> ;; eat2 dogs;; Spot eats. Rover eats. - : unit = ()
注意函数eat2奇怪的类型;它接受一个对象包含一个value方法,并产生一对 包含eat方法的对象。我们希望给它一个简单点的类型。
let eat2_both (animals : (animal, animal) mut_pair) = let x, y = animals#value in x#eat; y#eat;; val eat2_both : (animal, animal) mut_pair -> unit = <fun> ;; eat2_both dogs;; Characters 10-14: eat2_both dogs;; ^^^^ Error: This expression has type (dog, dog) mut_pair = < set_fst : dog -> unit; set_snd : dog -> unit; value : dog * dog > but an expression was expected of type (animal, animal) mut_pair = < set_fst : animal -> unit; set_snd : animal -> unit; value : animal * animal > Type dog = < bark : unit; eat : unit > is not compatible with type animal = < eat : unit > The second object type has no method bark
这里,我们遇到一个问题–函数eat2_both希望一对动物,但是我们传递给 它一对狗。当然,每条狗都是动物,所以或许我们只用一个强制类型转换来 转换pair到正确的类型。
let animals = (dogs : (dog, dog) mut_pair :> (animal, animal) mut_pair);; let animals = (dogs : (dog, dog) mut_pair :> (animal, animal) mut_pair);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type (dog, dog) mut_pair = < set_fst : dog -> unit; set_snd : dog -> unit; value : dog * dog > is not a subtype of (animal, animal) mut_pair = < set_fst : animal -> unit; set_snd : animal -> unit; value : animal * animal > Type animal = < eat : unit > is not a subtype of dog = < bark : unit; eat : unit >
这里又遇到了更多的问题。错误消息表示animal不是dog的子类型。
OCaml没有提供一个解决方案,但是为了理解它,我们需要看看变化标注 (variance annotations),它描述了什么样的强制转换对于多态类是合法的。
5.2.2 变化标注 (variance annotations)
OCaml在类型定义的参数上使用变化标注来指定它的子类化属性。一个参数 标注+'a表示'a的定义是协变,一个标注-'a表示这个'a的定义是逆变。单独 的参数'a表示'a的定义是不变的。当一个类型定义的时候,编译器检查标注 是否合法。
# type (+'a, +'b) pair' = 'a * 'b ;; type ('a, 'b) pair' = 'a * 'b # type (+'a, +'b) func' = 'a -> 'b ;; Characters 5-32: type (+'a, +'b) func' = 'a -> 'b ;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: In this definition, expected parameter variances are not satisfied. The 1st type parameter was expected to be covariant, but it is contravariant # type (-'a, +'b) func' = 'a -> 'b ;; type ('a, 'b) func' = 'a -> 'b
toploop在显示的输出中去掉了标注,但它仍然检查标注是否合法。
class [+'a, +'b] pair (x0 : 'a) (y0 : 'b) = object (self : 'self) val mutable x = x0 val mutable y = y0 method value : 'a * 'b = x, y end;; class ['a, 'b] pair : 'a -> 'b -> object val mutable x : 'a val mutable y : 'b method value : 'a * 'b end # let p = new pair (new dog "Spot") (new dog "Rover");; val p : (dog, dog) pair = <obj>
在前面我们希望强制转换一对dogs到一对animals。现在强转就可以工作了。
# (p :> (animal, animal) pair);; - : (animal, animal) pair = <obj>
这个会工作的原因在于pair的类类型在组成的类型中是协变的。因为dog是 animal的一个子类型,(dog, dog) pair是(animal, animal) pair的一个子 类型。
让我们指定同样的标注到可变pairs上。
class [+'a, +'b] mut_pair (x0 : 'a) (y0 : 'b) = object (self : 'self) val mutable x = x0 val mutable y = y0 method set_fst x' = x <- x' method set_snd y' = y <- y' method value = x, y end;; Error: In this definition, expected parameter variances are not satisfied. The 1st type parameter was expected to be covariant, but it is invariant
为什么不允许这个新定义?我们看看未标注类的方法类型:
class ['a, 'b] mut_pair : 'a -> 'b -> object ... method set_fst : 'a -> unit method set_snd : 'b -> unit method value : 'a * 'b end
问题在于类型参数'a和'b出现在箭头的左边,因此它是逆变的。另一个重要 的地方是'a * 'b类型中,它们是协变的。因为变量同时拥有逆变和协变, 它们必须是不变的。
5.2.3 正和负发生 (Positive and negative occurrences)
考虑如下类:
class [+'a, +'b] get_pair (x0 : 'a) (y0 : 'b) = object (self : 'self) val mutable x = x0 val mutable y = y0 method get_fst : ('a -> unit) -> unit = fun f -> f x method value = x, y end;; class ['a, 'b] get_pair : 'a -> 'b -> object val mutable x : 'a val mutable y : 'b method get_fst : ('a -> unit) -> unit method value : 'a * 'b end
这个类被接受了,尽管'a出现在了箭头的左边。
决定一个类型中的变量的变化有一个非常简单的计算方法。首先,对于有疑 问的类型变量出现的一些地方,我们按照类型定义中的箭头定义一个左递归 深度,当类型变量每次出现在一个完全由括号包围的类型中的箭头左边时,左递 归深度加1。协变构造器,比如*,不影响深度。类型构造器,比如 ref ,它指 定可变值需要的变量是不变的.
然后,认为这个变量是不变的.如果嵌套深度是偶数,则发生的地方就叫正的 (positive);如果是奇数,发生的地方就是负的(negative)。正的发生是协 变的,负的发生是逆变的。对于方法 get_fst : ('a -> unit) -> unit,'a的嵌套深度是2,这表示这个发生是正的和协变的。
5.2.4 通过隐藏进行强制转换
让我们回到mut_pair的例子。我们有一对dogs,并且希望强制转换这个对象 到一对animals。set_fst : 'a -> unit和set_snd : 'b -> unit妨碍了转 换,因为类型变量'a和'b出现在负的地方。然而,假如这些方法被忽略的话, 仍然可以强制转换这个类。
# let dogs = new mut_pair (new dog "Spot") (new dog "Rover");; val dogs : (dog, dog) mut_pair = <obj> # (dogs :> (animal, animal) pair);; - : (animal, animal) pair = <obj> # (dogs : (dog, dog) mut_pair :> (animal, animal) mut_pair);; Characters 0-57: (dogs : (dog, dog) mut_pair :> (animal, animal) mut_pair);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type (dog, dog) mut_pair = < set_fst : dog -> unit; set_snd : dog -> unit; value : dog * dog > is not a subtype of (animal, animal) mut_pair = value : animal * animal > Type animal = < eat : unit > is not a subtype of dog = < bark : unit; eat : unit >
另一个示例,使用不同的类型查看同一个对象。
class ['a] refcell (x0 : 'a) = object (self : 'self) val mutable x = x0 method set y = x <- y method get = x end;; class ['a] refcell : 'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end ;; class type [+'a] getcell = object method get : 'a end;; class type [-'a] setcell = object method set : 'a -> unit end;; class guard_dog name = object (self : 'self) inherit dog name method growl = Printf.printf "%s growls!\n" name end;; # let cell = new refcell (new dog "Spot");; val cell : dog refcell = <obj> # let read = (cell : (dog) refcell :> (animal) getcell);; val read : animal getcell = <obj> # let write = (cell : (dog) refcell :> (guard_dog) setcell);; val write : guard_dog setcell = <obj> # write#set (new guard_dog "Spike");; - : unit = () # let spike = read#get;; val spike : animal = <obj> # spike#eat;; Spike eats. - : unit = ()
5.3 类型约束
class [+'a] animal_pair (x0 : 'a) (y0 : 'a) = object (self : 'self) inherit ['a, 'a] pair x0 y0 constraint 'a = #animal method eat = x#eat; y#eat end;; class [+'a] animal_pair : 'a -> 'a -> object constraint 'a = #animal val mutable x : 'a val mutable y : 'a method eat : unit method value : 'a * 'a end # let dogs = new animal_pair (new dog "Spot") (new dog "Rover");; val dogs : dog animal_pair = <obj> # dogs#eat;; Spot eats. Rover eats. - : unit = ()
约束 constraint 'a = #animal 表示类型'a必须是一个animals的子类型。
class [+'a] dog_pair x0 y0 = object (self : 'self) inherit ['a] animal_pair x0 y0 constraint 'a = #dog method bark = x#bark; y#bark end;; class ['a] animal_pairs = object (self : 'self) val mutable pairs : 'a animal_pair list = [] method insert x0 y0 = pairs <- new animal_pair x0 y0 :: pairs method eat = List.iter (fun p -> p#eat) pairs end;; class ['a] animal_pairs : object constraint 'a = #animal val mutable pairs : 'a animal_pair list method eat : unit method insert : 'a -> 'a -> unit end
如果一个类包含类型约束,这个约束也要包含在类类型中。当然也可以强制 转换一个对象来移除类型约束,因为对象的类型已经固定了,并且这个约束 也已经满足了。
class type [+'a] read_only_animal_pairs_type = object method eat : unit end;; # let dogs = new animal_pairs;; val dogs : _#animal animal_pairs = <obj> # dogs#insert (new dog "Spot") (new dog "Fifi");; - : unit = () # dogs#insert (new dog "Rover") (new dog "Muffin");; - : unit = () # let animals = (dogs : dog animal_pairs :> animal read_only_animal_pairs_type);; val animals : animal read_only_animal_pairs_type = <obj> # animals#eat;; Rover eats. Muffin eats. Spot eats. Fifi eats.
5.4 比较对象和模块
OCaml提供了两个重要的抽象和重用工具:模块系统和对象系统。两个系统对 于大部分任务都能很好的完成,但有一些不同决定了你是用这个系统还是另一 个系统。
5.4.1 延迟绑定 (Late binding)
使用animals的示例,分别使用两个系统来实现。
(* Modules *) module type DogSig = sig type t val create : string -> t val name : t -> string val eat : t -> unit val bark : t -> unit val bark_eat : t -> unit end;; module Dog : DogSig = struct type t = string let create name = name let name dog = dog let eat dog = Printf.printf "%s eats.\n" (name dog) let bark dog = Printf.printf "%s barks!\n" (name dog) let bark_eat dog = bark dog; eat dog end;; (* Objects *) class type dog_type = object ('self) method name : string method eat : unit method bark : unit method bark_eat : unit end;; class dog name : dog_type = object (self : 'self) method name = name method eat = Printf.printf "%s eats.\n" self#name method bark = Printf.printf "%s barks!\n" self#name method bark_eat = self#bark; self#eat end;;
到此,两种实现只有一点不同;使用哪个实现主要是偏好问题。
现在,我们希望添加一个新类型的dog。
(* Modules *) module Hound : DogSig = struct include Dog let bark dog = Printf.printf "%s howls!\n" (name dog) end;; # let sam = Hound.create "Sam";; val sam : Hound.t = <abstr> # Hound.bark sam;; Sam howls! - : unit = () # Hound.bark_eat sam;; Sam barks! Sam eats. - : unit = () (* Objects *) class hound n : dog_type = object (self : 'self) inherit dog n method bark = Printf.printf "%s howls!\n" self#name end;; # let sam = new hound "Sam";; val sam : hound = <obj> # sam#bark;; Sam howls! - : unit = () # sam#bark_eat;; Sam howls! Sam eats. - : unit = ()
两个实现的行为不同。在模块中是静态作用域:一个标识符在程序文本的范围 中引用前面最近的定义。
与此相反,在对象中,方法调用类中最后的同名定义。
对这样的行为有好几个名字。对于对象叫做后期绑定(late binding),动 态方法派发(dynamic method dispatch)或开放递归(open recursion)。对于 模块叫做早期绑定(early binding),静态作用域(static scoping),或封闭 递归(closed recursion)。当希望后期绑定的时候,如例子所示,对象是首 选的解决方案。
另一个要注意的地方是,在模块的实现中,数据和使用数据的函数被分开了。 换句话说,程序员必须保证对hounds使用Hound模块。这被类型检查器强制 执行,因为类型Hound.t和Dog.t不同。如果我们希望的话,可以定义一个共 享约束 Hound : DogSig with type t = Dog.t 。这将允许Dog模块的任意 函数应用到hounds,这或许不是我们想要的。与此对比,hound类封装了数据 和它的方法;程序员不需要关心是否使用了正确的方法。
5.4.2 扩展定义
经常相信面向对象编程比“普通”函数式程序更容易修改和扩展。事实上, 并不总是这样–两种风格是不同的,并不能准确地比较。
让我们考虑定义一个带变量和求值器的计算器风格的语言作为示例.在函数 式方法中,我们使用联合类型定义这个语言;在面向对象的方法中,我们定义 一个表达式类.我们使用本章前面定义的Map数据类型来处理变量.
(* Modules *) module type EnvSig = sig type 'a t val empty : 'a t val add : 'a t -> string -> 'a -> 'a t val find : 'a t -> string -> 'a end;; module Env : EnvSig = struct type 'a t = (string * 'a) list let empty = [] let add env v x = (v, x) :: env let find env v = List.assoc v env end;; (* Objects *) class type ['a] env_sig = object ('self) method add : string -> 'a -> 'self method find : string -> 'a end;; class ['a] env : ['a] env_sig = object (self : 'self) val env : (string * 'a) list = [] method add v x = {< env = (v, x) :: env >} method find v = List.assoc v env end;;
一个环境是从变量到值的映射.两个实现非常相似.再次,这个选择是风格上 的.
下一步,我们定义这个语言.我们将包括常量,变量,一些基本算术运算,和一 个let绑定构造器.
(* Unions *) type exp = Int of int | Var of string | Add of exp * exp | If of exp * exp * exp | Let of string * exp * exp let rec eval env = function | Int i -> i | Var v -> Env.find env v | Add (e1, e2) -> eval env e1 + eval env e2 | If (e1, e2, e3) -> if eval env e1 <> 0 then eval env e2 else eval env e3 | Let (v, e1, e2) -> let i = eval env e1 in let env' = Env.add env v i in eval env' e2 (* let x = 3 in x + 4 *) # let e = Let ("x", Int 3, Add (Var "x", Int 4));; val e : exp = Let ("x", Int 3, Add (Var "x", Int 4)) # let i = eval Env.empty e;; val i : int = 7 (* Objects*) class type exp = object ('self) method eval : int env -> int end;; class int_exp (i : int) = object (self: 'self) method eval (_ : int env) = i end;; class var_exp v = object (self : 'self) method eval (env : int env) = env#find v end;; class add_exp (e1 : #exp) (e2 : #exp) = object (self : 'self) method eval env = e1#eval env + e2#eval env end;; class if_exp (e1 : #exp) (e2 : #exp) (e3 : #exp) = object (self : 'self) method eval env = if e1#eval env <> 0 then e2#eval env else e3#eval env end;; class let_exp (v : string) (e1 : #exp) (e2 : #exp) = object (self : 'self) method eval env = let i = e1#eval env in let env' = env#add v i in e2#eval env' end;; # let e = new let_exp "x" (new int_exp 3) (new add_exp (new var_exp "x") (new int_exp 4));; val e : let_exp = <obj> # let i = e#eval (new env);; val i : int = 7
使用联合的实现比使用对象的实现更短,除此之外基本相同.面向对象程序比 较长的原因在于每个类都要命名,每个定义都有一些开支.在大程序中,这些 开支无关紧要.
- 添加一个函数
它们如何进行扩展.假设我们希望添加一个新函数print输出一个表达式.对于联合,简单地添加 一个新函数,使用模式匹配进行定义.
open Printf let rec print chan = function | Int i -> fprintf chan "%d" i | Var v -> fprintf chan "%s" v | Add (e1, e2) -> fprintf chan "(%a + %a)" print e1 print e2 | If (e1, e2, e3) -> fprintf chan "if %a then %a else %a" print e1 print e2 print e3 | Let (v, e1, e2) -> fprintf chan "let %s = %a in %a" v print e1 print e2 ;; let e = Let ("x", Int 3, Add (Var "x", Int 4));; # print stdout e;; let x = 3 in (x + 4)- : unit = ()
对象的版本有点不同.这种情况下,我们必须为每种类型的表达式的类添加 一个新方法.这表示要么 1)我们修改每个类定义的源码 2)我们通过继承 定义新类来提供新实现. 这里使用后面的形式.
class type printable_exp = object ('self) inherit exp method print : out_channel -> unit end;; class printable_add_exp (e1 : #printable_exp) (e2 : #printable_exp) = object (self : 'self) inherit add_exp e1 e2 method print chan = fprintf chan "(%t + %t)" e1#print e2#print end ...
更新联合的实现明显比更新对象的实现更简单.添加新函数到联合的实现, 只是简单地添加它–不需要修改原始代码.
- 添加一个新的表达式类型
假如我们希望添加一个表达式表示两个表达式的乘积.这时,面向对象的方 法比较简单,只需要添加一个乘积的新对象.class printable_mul_exp (e1 : #printable_exp) (e2 : #printable_exp) = object (self : 'self) method eval env = e1#eval env * e2#eval env method print chan = fprintf chan "(%t * %t)" e1#print e2#print end;;
这里不需要修改原始代码.
作为对比,更新联合的定义更困难.我们必须更新原始类型定义来包含新情 况,另外,每个函数必须进行修改来处理新的表达式.
type exp = ... | Mul of exp * exp let rec eval env = function ... | Mul (e1, e2) -> eval env e1 * eval env e2 let rec print chan = function ... | Mul (e1, e2) -> fprintf chan "(%a * %a)" print e1 print e2
这个问题与为对象的实现添加新方法一样.
总结的不同点如下表:
Unions Objects 一个类型定义,为每种东西提供一个分支;每个操作一个函数 每种东西提供一个类,每个操作一个方法 添加一个函数 定义新函数,原始代码不用修改 修改每个类定义 添加一个新分支 修改类型定义,更新每个函数 定义新类,原始代码不用修改
- 模式匹配
联合的一个优点是模式匹配的良好支持.Footnotes:
1 动态查找在面向对象中经常叫做多态(polymorphism),但是它与 ML中的术语多态冲突。我们使用术语多态表示参数化的多态(类型多态),动态 查找表示对象多态。