Previous Contents Next

Types and Genericity

Besides the ability to model a problem using aggregation and inheritance relations, object-oriented programming is interesting because it provides the ability to reuse or modify the behavior of classes. However, extending an Objective CAML class must preserve the static typing properties of the language.

With abstract classes, you can factorize code and group their subclasses into one ``communication protocol''. An abstract class fixes the names and types of messages that may be received by instances of child classes. This technique will be better appreciated in connection with multiple inheritance.

The notion of an open object type (or simply an open type) that specifies the required methods allows code to work with instances using generic functions. But you may need to make the type constraints precise; this will be necessary for parameterized classes, which provide the genericity of parameterized polymorphism in the context of classes. With this latter object layer feature, Objective CAML can really be generic.

Abstract Classes and Methods

In abstract classes, some methods are declared without a body. Such methods are called abstract. It is illegal to instantiate an abstract class; new cannot be used. The keyword virtual is used to indicate that a class or method is abstract.

Syntax


class virtual name = object ...end
A class must be declared abstract as soon as one of its methods is abstract. A method is declared abstract by providing only the method type.

Syntax


method virtual name : type


When a subclass of an abstract class redefines all of the abstract methods of its parent, then it may become concrete; otherwise it also has to be declared abstract.

As an example, suppose we want to construct a set of displayable objects, all with a method print that will display the object's contents translated into a character string. All such objects need a method to_string. We define class printable. The string may vary according to the nature of the objects that we consider; therefore method to_string is abstract in the declaration of printable and consequently the class is also abstract.

# class virtual printable () =
object(self)
method virtual to_string : unit -> string
method print () = print_string (self#to_string())
end ;;
class virtual printable :
unit ->
object
method print : unit -> unit
method virtual to_string : unit -> string
end
We note that the abstractness of the class and of its method to_string is made clear in the type we obtain.

From this class, let us try to define the class hierarchy of figure 15.4.



Figure 15.4: Relations between classes of displayable objects.


It is easy to redefine the classes point, colored_point and picture by adding to their declarations a line inherit printable () that provides them with a method print through inheritance.



# let p = new point (1,1) in p#print() ;;
( 1, 1)- : unit = ()
# let pc = new colored_point (2,2) "blue" in pc#print() ;;
( 2, 2) with color blue- : unit = ()
# let t = new picture 3 in t#add (new point (1,1)) ;
t#add (new point (3,2)) ;
t#add (new point (1,4)) ;
t#print() ;;
[ ( 1, 1) ( 3, 2) ( 1, 4)]- : unit = ()


Subclass rectangle below inherits from printable, and defines method to_string. Instance variables llc (resp. urc) mean the lower left corner point (resp. upper right corner point) in the rectangle.

# class rectangle (p1,p2) =
object
inherit printable ()
val llc = (p1 : point)
val urc = (p2 : point)
method to_string () = "[" ^ llc#to_string() ^ "," ^ urc#to_string() ^ "]"
end ;;
class rectangle :
point * point ->
object
val llc : point
val urc : point
method print : unit -> unit
method to_string : unit -> string
end


Class rectangle inherits from the abstract class printable, and thus receives method print. It has two instance variables of type point: the lower left corner (llc) and upper right corner. Method to_string sends the message to_string to its point instance variables llc and urc.


# let r = new rectangle (new point (2,3), new point (4,5));;
val r : rectangle = <obj>
# r#print();;
[( 2, 3),( 4, 5)]- : unit = ()


Classes, Types, and Objects

You may remember that the type of an object is determined by the type of its methods. For instance, the type point, inferred during the declaration of class point, is an abbreviation for type:
  point =
    < distance : unit -> float; get_x : int; get_y : int;
      moveto : int * int -> unit; rmoveto : int * int -> unit;
      to_string : unit -> string  >
This is a closed type; that is, all methods and associated types are fixed. No additional methods and types are allowed. Upon a class declaration, the mechanism of type inference computes the closed type associated with class.

Open Types

Since sending a message to an object is part of the language, you may define a function that sends a message to an object whose type is still undefined.

# let f x = x#get_x ;;
val f : < get_x : 'a; .. > -> 'a = <fun>


The type inferred for the argument of f is an object type, since a message is sent to x, but this object type is open. In function f, parameter x must have at least a method get_x. Since the result of sending this message is not used within function f, its type has to be as general as possible (i.e. a variable of type 'a). So type inference allows the function f to be used with any object having a method get_x. The double points (..) at the end of the type < get_x : 'a; .. > indicate that the type of x is open.


# f (new point(2,3)) ;;
- : int = 2
# f (new colored_point(2,3) "emerald") ;;
- : int = 2
# class c () =
object
method get_x = "I have a method get_x"
end ;;
class c : unit -> object method get_x : string end
# f (new c ()) ;;
- : string = "I have a method get_x"


Type inference for classes may generate open types, particularly for initial values in instance construction. The following example constructs a class couple, whose initial values a and b have a method to_string.

# class couple (a,b) =
object
val p0 = a
val p1 = b
method to_string() = p0#to_string() ^ p1#to_string()
method copy () = new couple (p0,p1)
end ;;
class couple :
(< to_string : unit -> string; .. > as 'a) *
(< to_string : unit -> string; .. > as 'b) ->
object
val p0 : 'a
val p1 : 'b
method copy : unit -> couple
method to_string : unit -> string
end
The types of both a and b are open types, with method to_string. We note that these two types are considered to be different. They are marked ``as 'a'' and ``as 'b'', respectively. Variables of types 'a and 'b are constrained by the generated type.

We use the sharp symbol to indicate the open type built from a closed type obj_type:

Syntax


#obj_type
The type obtained contains all of the methods of type obj_type and terminates with a double point.

Type Constraints.

In the chapter on functional programming (see page ??), we showed how an expression can be constrained to have a type more precise than what is produced by inference. Object types (open or closed) can be used to enhance such constraints. One may want to open a priori the type of a defined object, in order to apply it to a forthcoming method. We can use an open object constraint:

Syntax


(name:#type)
Which allows us to write:

# let g (x : #point) = x#message;;
val g :
< distance : unit -> float; get_x : int; get_y : int; message : 'a;
moveto : int * int -> unit; print : unit -> unit;
rmoveto : int * int -> unit; to_string : unit -> string; .. > ->
'a = <fun>
The type constraint with #point forces x to have at least all of the methods of point, and sending message ``message'' adds a method to the type of parameter x.

Just as in the rest of the language, the object extension of Objective CAML provides static typing through inference. When this mechanism does not have enough information to determine the type of an expression, a type variable is assigned. We have just seen that this process is also valid for typing objects; however, it sometimes leads to ambiguous situations which the user must resolve by explicitly giving type information.

# class a_point p0 =
object
val p = p0
method to_string() = p#to_string()
end ;;
Characters 6-89:
Some type variables are unbound in this type:
class a_point :
(< to_string : unit -> 'b; .. > as 'a) ->
object val p : 'a method to_string : unit -> 'b end
The method to_string has type unit -> 'a where 'a is unbound


We resolve this ambiguity by saying that parameter p0 has type #point.

# class a_point (p0 : #point) =
object
val p = p0
method to_string() = p#to_string()
end ;;
class a_point :
(#point as 'a) -> object val p : 'a method to_string : unit -> string end


In order to set type constraints in several places in a class declaration, the following syntax is used:

Syntax


constraint type1 = type2
The above example can be written specifying that parameter p0 has type 'a, then putting a type constraint upon variable 'a.

# class a_point (p0 : 'a) =
object
constraint 'a = #point
val p = p0
method to_string() = p#to_string()
end ;;
class a_point :
(#point as 'a) -> object val p : 'a method to_string : unit -> string end


Several type constraints can be given in a class declaration.

Warning


An open type cannot appear as the type of a method.


This strong restriction exists because an open type contains an uninstantiated type variable coming from the rest of the type. Since one cannot have a free variable type in a type declaration, a method containing such a type is rejected by type inference.

# class b_point p0 =
object
inherit a_point p0
method get = p
end ;;
Characters 6-77:
Some type variables are unbound in this type:
class b_point :
(#point as 'a) ->
object val p : 'a method get : 'a method to_string : unit -> string end
The method get has type #point where .. is unbound


In fact, due to the constraint ``constraint 'a = #point'', the type of get is the open type #point. The latter contains a free variable type noted by a double point (..), which is not allowed.

Inheritance and the Type of self

There exists an exception to the prohibition of a type variable in the type of methods: a variable may stand for the type of the object itself (self). Consider a method testing the equality between two points.

# class point_eq (x,y) =
object (self : 'a)
inherit point (x,y)
method eq (p:'a) = (self#get_x = p#get_x) && (self#get_y = p#get_y)
end ;;
class point_eq :
int * int ->
object ('a)
val mutable x : int
val mutable y : int
method distance : unit -> float
method eq : 'a -> bool
method get_x : int
method get_y : int
method moveto : int * int -> unit
method print : unit -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
end
The type of method eq is 'a -> bool, but the type variable stands for the type of the instance at construction time.

You can inherit from the class point_eq and redefine the method eq, whose type is still parameterized by the instance type.

# class colored_point_eq (xc,yc) c =
object (self : 'a)
inherit point_eq (xc,yc) as super
val c = (c:string)
method get_c = c
method eq (pc : 'a) = (self#get_x = pc#get_x) && (self#get_y = pc#get_y)
&& (self#get_c = pc#get_c)
end ;;
class colored_point_eq :
int * int ->
string ->
object ('a)
val c : string
val mutable x : int
val mutable y : int
method distance : unit -> float
method eq : 'a -> bool
method get_c : string
method get_x : int
method get_y : int
method moveto : int * int -> unit
method print : unit -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
end


The method eq from class colored_point_eq still has type 'a -> bool; but now the variable 'a stands for the type of an instance of class colored_point_eq. The definition of eq in class colored_point_eq masks the inherited one. Methods containing the type of the instance in their type are called binary methods. They will cause some limitations in the subtyping relation described in page ??.

Multiple Inheritance

With multiple inheritance, you can inherit data fields and methods from several classes. When there are identical names for fields or methods, only the last declaration is kept, according to the order of inheritance declarations. Nevertheless, it is possible to reference a method of one of the parent classes by associating different names with the inherited classes. This is not true for instance variables: if an inherited class masks an instance variable of a previously inherited class, the latter is no longer directly accessible. The various inherited classes do not need to have an inheritance relation. Multiple inheritance is of interest because it increases class reuse.

Let us define the abstract class geometric_object, which declares two virtual methods compute_area and compute_peri for computing the area and perimeter.

# class virtual geometric_object () =
object
method virtual compute_area : unit -> float
method virtual compute_peri : unit -> float
end;;


Then we redefine class rectangle as follows:

# class rectangle_1 ((p1,p2) :'a) =
object
constraint 'a = point * point
inherit printable ()
inherit geometric_object ()
val llc = p1
val urc = p2
method to_string () =
"["^llc#to_string()^","^urc#to_string()^"]"
method compute_area() =
float ( abs(urc#get_x - llc#get_x) * abs(urc#get_y - llc#get_y))
method compute_peri() =
float ( (abs(urc#get_x - llc#get_x) + abs(urc#get_y - llc#get_y)) * 2)
end;;
class rectangle_1 :
point * point ->
object
val llc : point
val urc : point
method compute_area : unit -> float
method compute_peri : unit -> float
method print : unit -> unit
method to_string : unit -> string
end


This implementation of classes respects the inheritance graph of figure 15.5.



Figure 15.5: Multiple inheritance.


In order to avoid rewriting the methods of class rectangle, we may directly inherit from rectangle, as shown in figure 15.6.



Figure 15.6: Multiple inheritance (continued).


In such a case, only the abstract methods of the abstract class geometric_object must be defined in rectangle_2.

# class rectangle_2 (p2 :'a) =
object
constraint 'a = point * point
inherit rectangle p2
inherit geometric_object ()
method compute_area() =
float ( abs(urc#get_x - llc#get_x) * abs(urc#get_y - llc#get_y))
method compute_peri() =
float ( (abs(urc#get_x - llc#get_x) + abs(urc#get_y - llc#get_y)) * 2)
end;;


Continuing in the same vein, the hierarchies printable and geometric_object could have been defined separately, until it became useful to have a class with both behaviors. Figure 15.7 displays the relations defined in this way.



Figure 15.7: Multiple inheritance (end).


If we assume that classes printable_rect and geometric_rect define instance variables for the corners of a rectangle, we get class rectangle_3 with four points (two per corner).

class rectangle_3 (p1,p2) =
inherit printable_rect (p1,p2) as super_print
inherit geometric_rect (p1,p2) as super_geo
end;;
In the case where methods of the same type exist in both classes ..._rect, then only the last one is visible. However, by naming parent classes (super_...), it is always possible to invoke a method from either parent.

Multiple inheritance allows factoring of the code by integrating methods already written from various parent classes to build new entities. The price paid is the size of constructed objects, which are bigger than necessary due to duplicated fields, or inherited fields useless for a given application. Furthermore, when there is duplication (as in our last example), communication between these fields must be established manually (update, etc.). In the last example for class rectangle_3, we obtain instance variables of classes printable_rect and geometric_rect. If one of these classes has a method which modifies these variables (such as a scaling factor), then it is necessary to propagate these modifications to variables inherited from the other class. Such a heavy communication between inherited instance variables often signals a poor modeling of the given problem.

Parameterized Classes

Parameterized classes let Objective CAML's parameterized polymorphism be used in classes. As with the type declarations of Objective CAML, class declarations can be parameterized with type variables. This provides new opportunities for genericity and code reuse. Parameterized classes are integrated with ML-like typing when type inference produces parameterized types.

The syntax differs slightly from the declaration of parameterized types; type parameters are between brackets.

Syntax


class ['a, 'b, ...] name = object ...end
The Objective CAML type is noted as usual: ('a,'b,...) name.

For instance, if a class pair is required, a naive solution would be to set:

# class pair x0 y0 =
object
val x = x0
val y = y0
method fst = x
method snd = y
end ;;
Characters 6-106:
Some type variables are unbound in this type:
class pair :
'a ->
'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end
The method fst has type 'a where 'a is unbound


One again gets the typing error mentioned when class a_point was defined (page ??). The error message says that type variable 'a, assigned to parameter x0 (and therefore to x and fst), is not bound.

As in the case of parameterized types, it is necessary to parameterize class pair with two type variables, and force the type of construction parameters x0 and y0 to obtain a correct typing:

# class ['a,'b] pair (x0:'a) (y0:'b) =
object
val x = x0
val y = y0
method fst = x
method snd = y
end ;;
class ['a, 'b] pair :
'a ->
'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end
Type inference displays a class interface parameterized by variables of type 'a and 'b.

When a value of a parameterized class is constructed, type parameters are instantiated with the types of the construction parameters:

# let p = new pair 2 'X';;
val p : (int, char) pair = <obj>
# p#fst;;
- : int = 2
# let q = new pair 3.12 true;;
val q : (float, bool) pair = <obj>
# q#snd;;
- : bool = true


Note


In class declarations, type parameters are shown between brackets, but in types, they are shown between parentheses.


Inheritance of Parameterized Classes

When inheriting from a parameterized class, one has to indicate the parameters of the class. Let us define a class acc_pair that inherits from ('a,'b) pair; we add two methods for accessing the fields, get1 and get2,

# class ['a,'b] acc_pair (x0 : 'a) (y0 : 'b) =
object
inherit ['a,'b] pair x0 y0
method get1 z = if x = z then y else raise Not_found
method get2 z = if y = z then x else raise Not_found
end;;
class ['a, 'b] acc_pair :
'a ->
'b ->
object
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method snd : 'b
end
# let p = new acc_pair 3 true;;
val p : (int, bool) acc_pair = <obj>
# p#get1 3;;
- : bool = true


We can make the type parameters of the inherited parameterized class more precise, e.g. for a pair of points.

# class point_pair (p1,p2) =
object
inherit [point,point] pair p1 p2
end;;
class point_pair :
point * point ->
object
val x : point
val y : point
method fst : point
method snd : point
end


Class point_pair no longer needs type parameters, since parameters 'a and 'b are completely determined.

To build pairs of displayable objects (i.e. having a method print), we reuse the abstract class printable (see page ??), then we define the class printable_pair which inherits from pair.

# class printable_pair x0 y0 =
object
inherit [printable, printable] acc_pair x0 y0
method print () = x#print(); y#print ()
end;;


This implementation allows us to construct pairs of instances of printable, but it cannot be used for objects of another class with a method print.

We could try to open type printable used as a type parameter for acc_pair:

# class printable_pair (x0 ) (y0 ) =
object
inherit [ #printable, #printable ] acc_pair x0 y0
method print () = x#print(); y#print ()
end;;
Characters 6-149:
Some type variables are unbound in this type:
class printable_pair :
(#printable as 'a) ->
(#printable as 'b) ->
object
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method print : unit -> unit
method snd : 'b
end
The method fst has type #printable where .. is unbound
This first attempt fails because methods fst and snd contain an open type.

So we shall keep the type parameters of the class, while constraining them to the open type #printable.

# class ['a,'b] printable_pair (x0 ) (y0 ) =
object
constraint 'a = #printable
constraint 'b = #printable
inherit ['a,'b] acc_pair x0 y0
method print () = x#print(); y#print ()
end;;
class ['a, 'b] printable_pair :
'a ->
'b ->
object
constraint 'a = #printable
constraint 'b = #printable
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method print : unit -> unit
method snd : 'b
end


Then we construct a displayable pair containing a point and a colored point.

# let pp = new printable_pair
(new point (1,2)) (new colored_point (3,4) "green");;
val pp : (point, colored_point) printable_pair = <obj>
# pp#print();;
( 1, 2)( 3, 4) with color green- : unit = ()


Parameterized Classes and Typing

From the point of view of types, a parameterized class is a parameterized type. A value of such a type can contain weak type variables.

# let r = new pair [] [];;
val r : ('_a list, '_b list) pair = <obj>
# r#fst;;
- : '_a list = []
# r#fst = [1;2];;
- : bool = false
# r;;
- : (int list, '_a list) pair = <obj>


A parameterized class can also be viewed as a closed object type; therefore nothing prevents us from also using it as an open type with the sharp notation.

# let compare_nothing ( x : ('a, 'a) #pair) =
if x#fst = x#fst then x#mess else x#mess2;;
val compare_nothing :
< fst : 'a; mess : 'b; mess2 : 'b; snd : 'a; .. > -> 'b = <fun>


This lets us construct parameterized types that contain weak type variables that are also open object types.

# let prettytype x ( y : ('a, 'a) #pair) = if x = y#fst then y else y;;
val prettytype : 'a -> (('a, 'a) #pair as 'b) -> 'b = <fun>


If this function is applied to one parameter, we get a closure, whose type variables are weak. An open type, such as #pair, still contains uninstantiated parts, represented by the double point (..). In this respect, an open type is a partially known type parameter. Upon weakening such a type after a partial application, the displayer specifies that the type variable representing this open type has been weakened. Then the notation is _#pair.


# let g = prettytype 3;;
val g : ((int, int) _#pair as 'a) -> 'a = <fun>


Now, if function g is applied to a pair, its weak type is modified.

# g (new acc_pair 2 3);;
- : (int, int) acc_pair = <obj>
# g;;
- : (int, int) acc_pair -> (int, int) acc_pair = <fun>


Then we can no longer use g on simple pairs.

# g (new pair 1 1);;
Characters 4-16:
This expression has type (int, int) pair = < fst : int; snd : int >
but is here used with type
(int, int) acc_pair =
< fst : int; get1 : int -> int; get2 : int -> int; snd : int >
Only the second object type has a method get1


Finally, since parameters of the parameterized class can also get weakened, we obtain the following example.

# let h = prettytype [];;
val h : (('_b list, '_b list) _#pair as 'a) -> 'a = <fun>
# let h2 = h (new pair [] [1;2]);;
val h2 : (int list, int list) pair = <obj>
# h;;
- : (int list, int list) pair -> (int list, int list) pair = <fun>


The type of the parameter of h is no longer open. The following application cannot be typed because the argument is not of type pair.

# h (new acc_pair [] [4;5]);;
Characters 4-25:
This expression has type
('a list, int list) acc_pair =
< fst : 'a list; get1 : 'a list -> int list; get2 : int list -> 'a list;
snd : int list >
but is here used with type
(int list, int list) pair = < fst : int list; snd : int list >
Only the first object type has a method get1


Note


Parameterized classes of Objective CAML are absolutely necessary as soon as one has methods whose type includes a type variable different from the type of self.



Previous Contents Next