Previous Contents Next

Other Object-oriented Features

References: self and super

When defining a method in a class, it may be convenient to be able to invoke a method from a parent class. For this purpose, Objective CAML allows the object itself, as well as (the objects of) the parent class to be named. In the former case, the chosen name is given after the keyword object, and in the latter, after the inheritance declaration.

For example, in order to define the method to_string of colored points, it is better to invoke the method to_string from the parent class and to extend its behavior with a new method, get_color.

# class colored_point (x,y) c =
object (self)
inherit point (x,y) as super
val c = c
method get_color = c
method to_string () = super#to_string() ^ " [" ^ self#get_color ^ "] "
end ;;


Arbitrary names may be given to the parent and child class objects, but the names self and this for the current class and super for the parent are conventional. Choosing other names may be useful with multiple inheritance since it makes it easy to differentiate the parents (see page ??).

Warning


You may not reference a variable of an instance's parent if you declare a new variable with the same name since it masks the former.


Delayed Binding

With delayed binding the method used when a message is sent is decided at run-time; this is opposed to static binding where the decision is made at compile time. In Objective CAML, delayed binding of methods is used; therefore, the exact piece of code to be executed is determined by the recipient of the message.

The above declaration of class colored_point redefines the method to_string. This new definition uses method get_color from this class. Now let us define another class colored_point_1, inheriting from colored_point; this new class redefines method get_color (testing that the character string is appropriate), but does not redefine to_string.


# class colored_point_1 coord c =
object
inherit colored_point coord c
val true_colors = ["white"; "black"; "red"; "green"; "blue"; "yellow"]
method get_color = if List.mem c true_colors then c else "UNKNOWN"
end ;;


Method to_string is the same in both classes of colored points; but two objects from these classes will have a different behavior.

# let p1 = new colored_point (1,1) "blue as an orange" ;;
val p1 : colored_point = <obj>
# p1#to_string();;
- : string = "( 1, 1) [blue as an orange] "
# let p2 = new colored_point_1 (1,1) "blue as an orange" ;;
val p2 : colored_point_1 = <obj>
# p2#to_string();;
- : string = "( 1, 1) [UNKNOWN] "


The binding of get_color within to_string is not fixed when the class colored_point is compiled. The code to be executed when invoking the method get_color is determined from the methods associated with instances of classes colored_point and colored_point_1. For an instance of colored_point, sending the message to_string causes the execution of get_color, defined in class colored_point. On the other hand, sending the same message to an instance of colored_point_1 invokes the method from the parent class, and the latter triggers method get_color from the child class, controlling the relevance of the string representing the color.

Object Representation and Message Dispatch

An object is split in two parts: one may vary, the other is fixed. The varying part contains the instance variables, just as for a record. The fixed part corresponds to a methods table, shared by all instances of the class.

The methods table is a sparse array of functions. Every method name in an application is given a unique id that serves as an index into the methods table. We assume the existence of a machine instruction GETMETHOD(o,n), that takes two parameters: an object o and an index n. It returns the function associated with this index in the methods table. We write f_n for the result of the call GETMETHOD(o,n). Compiling the message send o#m computes the index n of the method name m and produces the code for applying GETMETHOD(o,n) to object o. This corresponds to applying function f_n to the receiving object o. Delayed binding is implemented through a call to GETMETHOD at run time.

Sending a message to self within a method is also compiled as a search for the index of the message, followed by a call to the function found in the methods table.

In the case of inheritance, since the method name always has the same index, regardless of redefinition, only the entry in new class' methods table is changed for redefinitions. So sending message to_string to an instance of class point will apply the conversion function of a point, while sending the same message to an instance of colored_point will find at the same index the function corresponding to the method which has been redefined to recognize the color field.

Thanks to this index invariance, subtyping (see page ??) is insured to be coherent with respect to the execution. Indeed if a colored point is explicitly constrained to be a point, then upon sending the message to_string the method index from class point is computed, which coincides with that from class colored_point. Searching for the method will be done within the table associated with the receiving instance, i.e. the colored_point table.

Although the actual implementation in Objective CAML is different, the principle of dynamic search for the method to be used is still the same.

Initialization

The class definition keyword initializer is used to specify code to be executed during object construction. An initializer can perform any computation and field access that is legal in a method.

Syntax


initializer expr


Let us again extend the class point, this time by defining a verbose point that will announce its creation.

# class verbose_point p =
object(self)
inherit point p
initializer
let xm = string_of_int x and ym = string_of_int y
in Printf.printf ">> Creation of a point at (%s %s)\n"
xm ym ;
Printf.printf " , at distance %f from the origin\n"
(self#distance()) ;
end ;;

# new verbose_point (1,1);;
>> Creation of a point at (1 1)
, at distance 1.414214 from the origin
- : verbose_point = <obj>


An amusing but instructive use of initializers is tracing class inheritance on instance creation. Here is an example:

# class c1 =
object
initializer print_string "Creating an instance of c1\n"
end ;;

# class c2 =
object
inherit c1
initializer print_string "Creating an instance of c2\n"
end ;;

# new c1 ;;
Creating an instance of c1
- : c1 = <obj>
# new c2 ;;
Creating an instance of c1
Creating an instance of c2
- : c2 = <obj>
Constructing an instance of c2 requires first constructing an instance of the parent class.

Private Methods

A method may be declared private with the keyword private. It will appear in the interface to the class but not in instances of the class. A private method can only be invoked from other methods; it cannot be sent to an instance of the class. However, private methods are inherited, and therefore can be used in definitions of the hierarchy3.

Syntax


method private name = expr


Let us extend the class point: we add a method undo that revokes the last move. To do this, we must remember the position held before performing a move, so we introduce two new fields, old_x and old_y, together with their update method. Since we do not want the user to have direct access to this method, we declare it as private. We redefine the methods moveto and rmoveto, keeping note of the current position before calling the previous methods for performing a move.

# class point_m1 (x0,y0) =
object(self)
inherit point (x0,y0) as super
val mutable old_x = x0
val mutable old_y = y0
method private mem_pos () = old_x <- x ; old_y <- y
method undo () = x <- old_x; y <- old_y
method moveto (x1, y1) = self#mem_pos () ; super#moveto (x1, y1)
method rmoveto (dx, dy) = self#mem_pos () ; super#rmoveto (dx, dy)
end ;;
class point_m1 :
int * int ->
object
val mutable old_x : int
val mutable old_y : int
val mutable x : int
val mutable y : int
method distance : unit -> float
method get_x : int
method get_y : int
method private mem_pos : unit -> unit
method moveto : int * int -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
method undo : unit -> unit
end


We note that method mem_pos is preceded by the keyword private in type point_m1. It can be invoked from within method undo, but not on another instance. The situation is the same as for instance variables. Even though fields old_x and old_y appear in the results shown by compilation, that does not imply that they may be handled directly (see page ??).

# let p = new point_m1 (0, 0) ;;
val p : point_m1 = <obj>
# p#mem_pos() ;;
Characters 0-1:
This expression has type point_m1
It has no method mem_pos
# p#moveto(1, 1) ; p#to_string() ;;
- : string = "( 1, 1)"
# p#undo() ; p#to_string() ;;
- : string = "( 0, 0)"


Warning


A type constraint may make public a method declared with attribute private.



Previous Contents Next