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.