This chapter gives an overview of the object-oriented features of Objective Caml.
The class point has one instance variable x and two methods get_x and move. The initial value of the instance variable is given here by the class parameter x_init. The variable x is declared mutable, so the method move can change its value.
# class point x_init = val mutable x = x_init method get_x = x method move d = x <- x + d end;; class point (int) = val mutable x : int method get_x : int method move : int -> unit end
We now create a new point p, giving the initialization argument 7.
# let p = new point 7;; val p : point = <obj>Note that the type of p is point. This is an abbreviation automatically defined by the class definition above. It stands for the object type < get_x : int; move : int -> unit>, listing the methods of class point along with their types.
Let us apply some methods to p:
# p#get_x;; - : int = 7 # p#move 3;; - : unit = () # p#get_x;; - : int = 10
The library function Oo.copy copies an object. Its type is < .. > as 'a -> 'a. The keyword as in that type binds the type variable 'a to the object type < .. >. Oo.copy therefore takes an object with any methods (represented by the ellipsis), and returns an object of the same type. The type of Oo.copy is different from type < .. > -> < .. > as each ellipsis represents a different set of methods. Ellipsis actually behaves as a type variable.
# let q = Oo.copy p;; val q : point = <obj> # q#move 7; (p#get_x, q#get_x);; - : int * int = 10, 17
We now define a new class color_point. This class inherits from class point. So, it has all the instance variable and all the methods of point, plus a new instance variable c and a new method color.
# class color_point x (c : string) = inherit point x val c = c method color = c end;; class color_point (int) (string) = val c : string val mutable x : int method get_x : int method move : int -> unit method color : string end # let p' = new color_point 5 "red";; val p' : color_point = <obj> # p'#get_x, p'#color;; - : int * string = 5, "red"
A point and a color point have incompatible types: a point has no method color. Thus, if one wants to put p and p' in the same list, one needs to coerce p' to the type of points, hiding its color method.
# let l = [p; (p' :> point)];; val l : point list = [<obj>; <obj>]
The function get_x below is a generic function applying method get_x to any object p which has this method (and possibly some others, which are represented by an ellipsis in the type). The method needs not be declared previously, as shown by the companion function set_x. Function get_x can then be mapped on list l.
# let get_x p = p#get_x;; val get_x : < get_x : 'a; .. > -> 'a = <fun> # let set_x p = p#set_x;; val set_x : < set_x : 'a; .. > -> 'a = <fun> # List.map get_x l;; - : int list = [10; 5]
Reference cells can also be implemented as objects:
# class ref x_init = val mutable x = x_init method get = x method set y = x <- y end;; Characters 5-85: The type variable 'a is not bound in implicit type definition ref = < get : 'a; set : 'a -> unit > It should be captured by a class type parameterThe reason why this definition does not typecheck is that at least one of the methods has a polymorphic type (here, the type of the value stored in the reference cell), thus the class should be parametric. A monomorphic instance of the class could be defined by:
# class ref (x_init:int) = val mutable x = x_init method get = x method set y = x <- y end;; class ref (int) = val mutable x : int method get : int method set : int -> unit endA class for polymorphic references must explicitly list the type parameters in its declaration. The type parameters must also be bound somewhere in the class body by a type constraint.
# class 'a ref x_init = val mutable x = (x_init : 'a) method get = x method set y = x <- y end;; class 'a ref ('a) = val mutable x : 'a method get : 'a method set : 'a -> unit end # let r = new ref 1 in r#set 2; (r#get);; - : int = 2The type parameter in the declaration may actually be constrained in the body of the class definition. In the class type, the actual value of the type parameter is displayed in the constraint clause.
# class 'a ref (x_init:'a) = val mutable x = x_init + 1 method get = x method set y = x <- y end;; class 'a ref ('a) = constraint 'a = int val mutable x : int method get : int method set : int -> unit end
Let us consider a more realistic example. We put an additional type constraint in method move, since no free variables must remain uncaptured by a type parameter.
# class 'a circle (c : 'a) = val mutable center = c method center = center method set_center c = center <- c method move = (center#move : int -> unit) end;; class 'a circle ('a) = constraint 'a = < move : int -> unit; .. > val mutable center : 'a method center : 'a method set_center : 'a -> unit method move : int -> unit end
An alternate definition of circle, using a constraint clause in the class definition, is shown below. The type #point used below in the constraint clause is an abbreviation produced by the definition of class point. This abbreviation unifies with the type of any object belonging to a subclass of class point. It actually expands to < get_x : int; move : int -> unit; .. >. This leads to the following alternate definition of circle, which has slightly stronger constraints on its argument, as we now expect center to have a method get_x.
# class 'a circle (c : 'a) = constraint 'a = #point val mutable center = c method center = center method set_center c = center <- c method move = center#move end;; class 'a circle ('a) = constraint 'a = #point val mutable center : 'a method center : 'a method set_center : 'a -> unit method move : int -> unit end
The class color_circle is a specialized version of class circle which requires the type of the center to unify with #color_point, and adds a method color.
# class 'a color_circle c = constraint 'a = #color_point inherit ('a) circle c method color = center#color end;; class 'a color_circle ('a) = constraint 'a = #color_point val mutable center : 'a method center : 'a method set_center : 'a -> unit method move : int -> unit method color : string end
A method can also send messages to the object that invoked the method. For that, self must be explicitly bound, here to the variable s.
# class printable_point y as s = inherit point y method print = print_int s#get_x end;; class printable_point (int) = val mutable x : int method get_x : int method move : int -> unit method print : unit end # let p = new printable_point 7;; val p : printable_point = <obj> # p#print;; 7- : unit = ()The variable s is bound at the invocation of a method. In particular, if the class printable_point is inherited, the variable s will correctly be bound to an object of the subclass.
Multiple inheritance is allowed. Only the last definition of a method (or of an instance variable) is kept. But previous definitions of a method can be reused by binding the related ancestor. Here, super is bound to the ancestor printable_point. The name super is not actually a variable and can only be used to select a method as in super#print.
# class printable_color_point y c as self = inherit color_point y c inherit printable_point y as super method print = print_string "("; super#print; print_string ", "; print_string (self#color); print_string ")" end;; class printable_color_point (int) (string) = val c : string val mutable x : int method get_x : int method move : int -> unit method color : string method print : unit end # let p' = new printable_color_point 7 "red";; val p' : printable_color_point = <obj> # p'#print;; (7, red)- : unit = ()
It is possible to write a version of class point without assignments on the instance variables. The construct {< ... >} returns a copy of ``self'' (that is, the current object), possibly changing the value of some instance variables.
# class functional_point y = val x = y method get_x = x method move d = {< x = x + d >} end;; class functional_point (int) : 'a = val x : int method get_x : int method move : int -> 'a end # let p = new functional_point 7;; val p : functional_point = <obj> # p#get_x;; - : int = 7 # (p#move 3)#get_x;; - : int = 10 # p#get_x;; - : int = 7Note that the type abbreviation functional_point is recursive, which can be seen in the class type of functional_point: the type of self to 'a and 'a appears inside the type of the move method.
The class comparable below is a template for classes with a binary method leq of type 'a -> bool where the type variable 'a is bound to the type of self. Since this class has a method declared but not defined, it must be flagged virtual and cannot be instantiated (that is, no object of this class can be created). It still defines abbreviations. In particular, #comparable expands to < leq : 'a -> bool; .. > as 'a. We see here that the binder as also allows to write recursive types.
# class virtual comparable () : 'a = virtual leq : 'a -> bool end;; class virtual comparable (unit) : 'a = virtual leq : 'a -> bool end
We then defines a subclass of comparable, which wraps integers as comparable objects. There is a type constraint on the class parameter x as the primitive <= is a polymorphic comparison function in Objective Caml. The inherit clause ensures that the type of objects of this class is an instance of #comparable.
# class int_comparable (x : int) = inherit comparable () val x = x method x = x method leq p = x <= p#x end;; class int_comparable (int) : 'a = val x : int method leq : 'a -> bool method x : int end
Objects of class int_comparable2 below can also modify the integer they hold. The status of instance variable x is changed. It is now mutable and private; that is, subclasses cannot access it (it does no longer appear in the class type). Note that the type int_comparable2 is not a subtype of type int_comparable, as the self type appears in contravariant position in the type of method leq.
# class int_comparable2 x = inherit int_comparable x val private mutable x method set_x y = x <- y end;; class int_comparable2 (int) : 'a = method leq : 'a -> bool method x : int method set_x : int -> unit end
The function min will return the minimum of any two objects whose type unify with #comparable. The type of min is not the same as #comparable -> #comparable -> #comparable, as the abbreviation #comparable hides a type variable (an ellipsis). Each occurrence of this abbreviation generates a new variable.
# let min (x : #comparable) y = if x#leq y then x else y;; val min : (#comparable as 'a) -> 'a -> 'a = <fun>This function can be applied to objects of type int_comparable or int_comparable2.
# (min (new int_comparable 7) (new int_comparable 11))#x;; - : int = 7 # (min (new int_comparable2 5) (new int_comparable2 3))#x;; - : int = 3
This last example is somewhat more complicated. It demonstrates recursive classes. Indeed, lists can also be implemented by classes. We recursively define three classes:
# class virtual 'a lst () as self = virtual null : bool virtual hd : 'a virtual tl : 'a lst method map f = (if self#null then new nil () else new cons (f self#hd) (self#tl#map f) : 'a lst) method iter (f : 'a -> unit) = if self#null then () else begin f self#hd; self#tl#iter f end method print (f : 'a -> unit) = print_string "("; self#iter (fun x -> f x; print_string "::"); print_string "[]"; print_string ")" and 'a nil () = inherit ('a) lst () method null = true method hd = failwith "hd" method tl = failwith "tl" and 'a cons h t = inherit ('a) lst () val h = h val t = t method null = false method hd = h method tl = t end;; class virtual 'a lst (unit) = method map : ('a -> 'a) -> 'a lst method iter : ('a -> unit) -> unit method print : ('a -> unit) -> unit virtual null : bool virtual hd : 'a virtual tl : 'a lst end class 'a nil (unit) = method null : bool method hd : 'a method tl : 'a lst method map : ('a -> 'a) -> 'a lst method iter : ('a -> unit) -> unit method print : ('a -> unit) -> unit end class 'a cons ('a) ('a lst) = val h : 'a val t : 'a lst method null : bool method hd : 'a method tl : 'a lst method map : ('a -> 'a) -> 'a lst method iter : ('a -> unit) -> unit method print : ('a -> unit) -> unit endIt is a weakness of Objective Caml that objects cannot have polymorphic methods: the method map can only return a list of the same type as the objet to which it applies.
# let l1 = new cons 3 (new cons 10 (new nil ()));; val l1 : int cons = <obj> # l1#print print_int;; (3::10::[])- : unit = () # let l2 = l1#map (fun x -> x + 1);; val l2 : int lst = <obj> # l2#print print_int;; (4::11::[])- : unit = ()A polymorphic map function can still be defined:
# let rec map_list f (x:'a lst) = if x#null then new nil() else new cons (f x#hd) (map_list f x#tl);; val map_list : ('a -> 'b) -> 'a lst -> 'b lst = <fun> # let p1 = (map_list (fun x -> new printable_color_point x "red") l1);; val p1 : printable_color_point lst = <obj> # p1#print (fun x -> x#print);; ((3, red)::(10, red)::[])- : unit = ()
Here are two common problems. The first one is that, in a class definition, coercion to the type defined by this class is the identity. The reason is that this type abbreviation is not yet completely defined, and thus its subtypes are not clearly known. So, for instance, the following class fails to type. Indeed, the type of self is unified to the closed type c.
# class c () as self = method m = (self : #c :> c) end;; Characters 5-48: The class c is closed, but not marked closedThe abbreviation should first be defined by an virtual class, before being used:
# class virtual c () = virtual m : c end;; class virtual c (unit) = virtual m : c end # class c' () as self = inherit c () method m = (self : #c :> c) end;; class c' (unit) = method m : c endThe second problem is that the coercion operator (e :> t) is not always powerful enough. Here is an example:
# class virtual c () = virtual m : c end;; class virtual c (unit) = virtual m : c end # class c' () as self = inherit c () method m = (self :> c) method m' = 1 end;; Characters 51-55: This expression cannot be coerced to type c = < m : c >; it has type < m : c; m' : 'a; .. > but is here used with type < m : 'b; m' : 'a; .. > as 'b Type c = < m : c > is not compatible with type 'bThe operator type can be seen here:
# function x -> (x :> c);; - : (< m : 'a; .. > as 'a) -> c = <fun>So, as class c' inherits from class c, its method m has type c, but then according to the operator type, expression (self :> c) constrains this type to be also the one of self. This fails, as c has no method m'. On the other hand, #c = < m : c; .. >, so one can use the more precise coercion operator:
# class c' () as self = inherit c () method m = (self : #c :> c) end;; class c' (unit) = method m : c endAn alternative is to rather defines class c as follow (of course this definition is not equivalent to the previous one):
# class virtual c () : 'a = virtual m : 'a end;; class virtual c (unit) : 'a = virtual m : 'a endThen a coercion operator is not even required.
# class c' () as self = method m = self method m' = 1 end;; class c' (unit) : 'a = method m : 'a method m' : int endMoreover, the simple coercion operator (e :> t) can be used to coerce an object of type c' to type c. It is actually defined so as to work fine with classes returning self without coercion.
# (new c' () :> c);; - : c = <obj>