|
The aim of this chapter is to present STklos object
system. Briefly stated, STklos gives the programmer
an extensive object system with meta-classes, multiple
inheritance, generic functions and multi-methods. Furthermore,
its implementation relies on a Meta Object Protocol
(MOP) [9], in the spirit of the one defined for
CLOS [10]. STklos implementation is derived from the version 1.3
of Tiny CLOS, a pure and clean CLOS-like MOP
implementation in Scheme written by Gregor Kickzales
[8]. However, Tiny CLOS implementation was
designed as a pedagogical tool and consequently, completeness and
efficiency were not the author concern for it. STklos
extends the Tiny CLOS model to be efficient and as close as
possible to CLOS, the Common Lisp Object System [10].
Some features of STklos are also issued from
Dylan [4] or SOS [6]. This chapter is divided in three parts, which have a
quite different audience in mind: - The first part presents the STklos object system rather
informally; it is intended to be a tutorial of the language and is
for people who want to have an idea of the look and feel of
STklos.
- The second part describes the STklos object system at
the external level (i.e. without requiring the use of the
Meta Object Protocol).
- The third and last part describes the STklos Meta
Object Protocol. It is intended for people whio want to play with
meta programming.
8.2 Object System Tutorial
The STklos object system relies on classes like most of
the current OO languages. Furthermore, STklos provides
meta-classes, multiple inheritance, generic functions and
multi-methods as in CLOS, the Common Lisp Object System
[10] or Dylan [4]. This chapter
presents STklos in a rather informal manner. Its intent is to
give the reader an idea of the "look and feel" of
STklos programming. However, we suppose here that the reader
has some basic notions of OO programming, and is familiar with
terms such as classes, instances or methods.
8.2.1 Class definition and instantiation
A new class is defined with the define-class form.
The syntax of define-class is close to CLOS defclass :
(define-class class (superclass1 superclass2 ...)
(slot-description1
slot-description2
...)
metaclass option)
|
The metaclass option will not be discussed here.
The superclasses list specifies the super classes of
class
(see inheritance for details). A slot description gives the name of a slot and,
eventually, some "properties" of this slot (such as its
initial value, the function which permit to access its value,
...). Slot descriptions will be discussed in
slot-definition. As an example, consider now that we want to define a
point as an object. This can be done with the following class
definition:
(define-class <point> ()
(x y))
|
This definition binds the symbol <point> to a
new class whose instances contain two slots. These slots are
called x an y and we suppose here that they
contain the coordinates of a 2D point. Let us define now a circle, as a 2D point and a radius:
(define-class <circle> (<point>)
(radius))
|
As we can see here, the class <circle> is
constructed by inheriting from the class <point> and
adding a new slot (the radius slot).
8.2.1.2 Instance creation and slot access
Creation of an instance of a previously defined class can
be done with the make procedure. This procedure takes
one mandatory parameter which is the class of the instance which
must be created and a list of optional arguments. Optional
arguments are generally used to initialize some slots of the
newly created instance. For instance, the following form:
(define c (make <circle>))
|
creates a new <circle> object and binds it to the
c Scheme variable. Accessing the slots of the newly created circle can be done
with the slot-ref and the slot-set!
primitives. The slot-set! primitive permits to set the
value of an object slot and slot-ref permits to get its
value.
(slot-set! c 'x 10)
(slot-set! c 'y 3)
(slot-ref c 'x) ⇒ 10
(slot-ref c 'y) ⇒ 3
|
Using the describe function is a simple way
to see all the slots of an object at one time: this function
prints all the slots of an object on the standard output. For
instance, the expression:
prints the following informations on the standard output:
#[<circle> 81aa1f8] is an an instance of class <circle>.
Slots are:
radius = #[unbound]
x = 10
y = 3
|
When specifying a slot, a set of options can be given to
the system. Each option is specified with a keyword. For
instance, - :init-form can be used to supply a default
value for the slot.
- :init-keyword can be used to specify the
keyword used for initializing a slot.
- :getter can be used to define the name of the
slot getter
- :setter can be used to define the name of the
slot setter
:accessor can be used to define the name of the
slot accessor (see below) To illustrate slot description, we redefine here the
<point> class seen before. A new definition of this
class could be:
(define-class <point> ()
((x :init-form 0 :getter get-x :setter set-x! :init-keyword :x)
(y :init-form 0 :getter get-y :setter set-y! :init-keyword :y)))
|
With this definition, the x and y slots are
set to 0 by default. Value of a slot can also be specified by
calling make with the :x and :y
keywords. Furthermore, the generic functions get-x and
set-x!
(resp. get-y and set-y! ) are automatically
defined by the system to read and write the x (resp.
y ) slot.
(define p1 (make <point> :x 1 :y 2))
(get-x p1) ⇒ 1
(set-x! p1 12)
(get-x p1) ⇒ 2
(define p2 (make <point> :x 2))
(get-x p2) ⇒ 2
(get-y p2) ⇒ 0
|
Accessors provide an uniform access for reading and writing an object slot.
Writing a slot is done with an extended form of set! which is
close to the Common Lisp setf macro. A slot accessor can be
defined with the :accessor option in the slot
description. Hereafter, is another definition of our
<point> class, using an accessor:
(define-class <point> ()
((x :init-form 0 :accessor x-of :init-keyword :x)
(y :init-form 0 :accessor y-of :init-keyword :y)))
|
Using this class definition, reading the x coordinate of the
p point can be done with:
and setting it to 100 can be done using the extended set!
Note: STklos also define slot-set! as the setter
function of slot-ref (see setter).
As a consequence, we have:
(set! (slot-ref p 'y) 100)
(slot-ref p 'y) ⇒ 100
|
Suppose that we need a slot named area in circle
objects which contain the area of the circle. One way to do this
would be to add the new slot to the class definition and have an
initialisation form for this slot which takes into account the
radius of the circle. The problem with this approach is that if
the radius slot is changed, we need to change
area (and vice-versa). This is something which is hard
to manage and if we don't care, it is easy to have a area
and radius in an instance which are "un-synchronized".
The virtual slot mechanism avoid this problem. A virtual slot is a special slot whose value is calculated
rather than stored in an object. The way to read and write such
a slot must be given when the slot is defined with the
:slot-ref and :slot-set! slot options. A complete definition of the <circle> class
using virtual slots could be:
(define-class <circle> (<point>)
((radius :init-form 0 :accessor radius :init-keyword :radius)
(area :allocation :virtual :accessor area
:slot-ref (lambda (o)
(let ((r (radius o)))
(* 3.14 r r)))
:slot-set! (lambda (o v)
(set! (radius o) (sqrt (/ v 3.14)))))))
|
Here is an example using this definition of <circle>
(define c (make <circle> :radius 1))
(radius c) ⇒ 1
(area c) ⇒ 3.14
(set! (area x) (* 4 (area x)))
(area c) ⇒ 12.56
(radius c) ⇒ 2.0
|
Of course, we can also used the fucntion describe to visualize
the slots of a given object. Applied to the prvious c , it prints:
#[<circle> 81b2348] is an an instance of class <circle>.
Slots are:
area = 12.56
radius = 2.0
x = 0
y = 0
|
8.2.2.1 Class hierarchy and inheritance of slots
Inheritance is specified upon class definition. As said in
the introduction, STklos supports multiple inheritance.
Hereafter are some classes definition:
(define-class A () (a))
(define-class B () (b))
(define-class C () (c))
(define-class D (A B) (d a))
(define-class E (A C) (e c))
(define-class F (D E) (f))
|
A , B , C have a null list of
super classes. In this case, the system will replace it by the
list which only contains <object> , the root of all the
classes defined by define-class . D ,
E , and F use multiple inheritance: each class
inherits from two previously defined classes. Those class
definitions define a hierarchy which is shown in figure
1.
In this figure, the class <top> is also shown; this
class is the super class of all Scheme objects. In particular,
<top> is the super class of all standard Scheme
types.
Fig. 1: a class hierarchy
The set of slots of a given class is calculated by
"unioning" the slots of all its super class. For instance,
each instance of the class D defined before will have three
slots (a , b and d ). The slots of a
class can be obtained by the class-slots primitive. For
instance,
(class-slots A) ⇒ (a)
(class-slots E) ⇒ (a e c)
(class-slots F) ⇒ (b e c d a f)
|
Note: The order of slots is not significant.
8.2.2.2 Class precedence list
A class may have more than one superclass.1
With single inheritance (only one superclass), it is easy to
order the super classes from most to least specific. This is the
rule: Rule 1: Each class is more specific than
its superclasses.
With multiple inheritance, ordering is harder. Suppose we
have
(define-class X ()
((x :init-form 1)))
(define-class Y ()
((x :init-form 2)))
(define-class Z (X Y)
(z :init-form 3))
|
In this case, given Rule 1, the Z class is more
specific than the X or Y class for instances
of Z . However, the :init-form specified in
X and Y leads to a problem: which one
overrides the other? Or, stated differently, which is the
default initial value of the x slot of a Z
instance. The rule in STklos, as in CLOS, is that the
superclasses listed earlier are more specific than those listed
later. So: Rule 2: For a given class, superclasses listed earlier
are more specific than those listed later.
These rules are used to compute a linear order for a
class and all its superclasses, from most specific to least
specific. This order is called the "class precedence
list" of the class. Given these two rules, we can claim that
the initial form for the x slot of previous example
is 1 since the class X is placed before Y in the
super classes of Z . These two rules are not always
sufficient to determine a unique order. However, they give an
idea of how the things work. STklos algorithm for
calculating the class precedence list of a class is a little
simpler than the CLOS one described in (ref :bib "AMOP") for breaking
ties. Consequently, the calculated class precedence list by
STklos algorithm can be different than the one given by
the CLOS one in some subtle situations. Taking
the F class shown in Figure 1,
the STklos calculated class precedence list is
(f d e a b c <object> <top>)
|
whereas it would be the following list with a CLOS-like
algorithm:
(f d e a c b <object> <top>)
|
However, it is usually considered a bad idea for programmers
to rely on exactly what the order is. If the order for some
superclasses is important, it can be expressed directly in the
class definition. The precedence list of a class can be obtained
by the function class-precedence-list . This function
returns a ordered list whose first element is the most specific
class. For instance,
(class-precedence-list D)
⇒ (#[<class> d 81aebb8] #[<class> a 81aab88]
#[<class> b 81aa720] #[<class> <object> 80eff90]
#[<class> <top> 80effa8])
|
However, this result is not too much readable; using the
function class-name yields a clearer result:
(map class-name (class-precedence-list D))
⇒ (d a b <object> <top>)
|
8.2.3.1 Generic functions and methods
Neither STklos nor CLOS use the message passing mechanism
for methods as most Object Oriented languages do. Instead, they
use the notion of generic function.A generic function
can be seen as a "tanker" of methods. When the
evaluator requests the application of a generic function, all the
applicable methods of this generic function will be grabbed and
the most specific among them will be applied. We say that a
method M is more specific than a method M'
if the class of its parameters are more specific than the M'
ones. To be more precise, when a generic function must be
"called" the system - searchs among all the generic function methods those which
are applicable (i.e. the ones which filter on types which are
compatible with the actual argument list),
- sorts the list of applicable methods in the "most specific"
order,
- calls the most specific method of this list (i.e. the
first of the list of sorted methods).
The definition of a generic function is done with the
define-generic macro. Definition of a new method is
done with the define-method macro. Consider the following definitions:
(define-generic M)
(define-method M((a <integer>) b) 'integer)
(define-method M((a <real>) b) 'real)
(define-method M(a b) 'top)
|
The define-generic call defines M as a
generic function. Note that the signature of the generic
function is not given upon definition, contrarily to CLOS. This
permits methods with different signatures for a given generic
function, as we shall see later. The three next lines define
methods for the M generic function. Each method uses a
sequence of parameter specializers that specify when
the given method is applicable. A specializer permits to
indicate the class a parameter must belong (directly or
indirectly) to be applicable. If no specializer is given, the
system defaults it to <top> >. Thus, the first method
definition is equivalent to
(define-method M((a <integer>) (b <top>)) 'integer)
|
Now, let us look at some possible calls to generic
function M :
(M 2 3) ⇒ integer
(M 2 #t) ⇒ integer
(M 1.2 'a) ⇒ real
(M #t #f) ⇒ top
(M 1 2 3) ⇒ error no method with 3 parameters
|
The preceding methods use only one specializer per parameter
list. Of course, each parameter can use a specializer. In this
case, the parameter list is scanned from left to right to
determine the applicability of a method. Suppose we declare now
(define-method M ((a <integer>) (b <number>))
'integer-number)
(define-method M ((a <integer>) (b <real>))
'integer-real)
(define-method M (a (b <number>))
'top-number)
(define-method M (a b c)
'three-parameters)
|
In this case,
(M 1 2) ⇒ integer-integer
(M 1 1.0) ⇒ integer-real
(M 'a 1) ⇒ top-number
(M 1 2 3) ⇒ three-parameters
|
Notes: - Before defining a new generic
function
define-generic, verifies if the symbol given as
parameter is already bound to a procedure in the current
environment. If so, this procedure is added, as a method to the
newly created generic function.
For instance:
(define-generic log)
(define-method log ((s <string>) . l)
(apply format (current-error-port) s l)
(newline (current-error-port)))
(log "Hello, ~a" "world") -| Hello, world
(log 1) ⇒ 0
|
define-method automatically defines the
generic function if it has not been defined before.
Consequently, most of the time, the define-generic is
not needed.
When a generic function is called, the list of applicable
methods is built. As mentioned before, the most specific method
of this list is applied (see
Generic functions and methods).
This method may call, if needed, the next method in the list of
applicable methods. This is done by using the special form
next-method . Consider the following definitions
(define-method Test((a <integer>))
(cons 'integer (next-method)))
(define-method Test((a <number>))
(cons 'number (next-method)))
(define-method Test(a)
(list 'top))
|
With those definitions, we have:
(Test 1) ⇒ (integer number top)
(Test 1.0) ⇒ (number top)
(Test #t) ⇒ (top)
|
8.2.3.3 Standard generic functions
Printing objects When the Scheme primitives write or
display are called with a parameter which is an object,
the write-object or display-object generic
functions are called with this object and the port to which the
printing must be done as parameters. This facility permits to
define a customized printing for a class of objects by simply
defining a new method for this class. So, defining a new
printing method overloads the standard printing method
(which just prints the class of the object and its hexadecimal
address). For instance, we can define a customized printing for the <point>
used before as:
(define-method display-object ((p <point>) port)
(format port "{Point x=~S y=~S}" (slot-ref p 'x) (slot-ref p 'y)))
|
With this definition, we have
(define p (make <point> :x 1 :y 2))
(display p) -| {Point x=1 y=2}
|
The Scheme primitive write tries to write
objects, in such a way that they are readable back with the read
primitive. Consequently, we can define the writing of a <point>
as a form which, when read, will build back this point:
(define-method write-object ((p <point>) port)
(format port "#,(make <point> :x ~S :y ~S)"
(get-x p) (get-y p)))
|
With this method, writing the p point defined before
prints the following text on the output port:
#,(make <point> :x 1 :y 2)
|
Note here the usage of the "#, " notation of SRFI-10
(Sharp Comma External Form)
to "evaluate" the form when reading it2. Comparing objects When objects are compared with the eqv? or
equal? Scheme standard primitives, STklos
calls the object-eqv? or object-equal?
generic functions. This facility permits to define a
customized comparison function for a class of objects by simply
defining a new method for this class. Defining a new
comparison method overloads the standard comparaison method
(which always returns #f ). For instance we could
define the following method to compare points:
(define-method object-eqv? ((a <point>) (b <point>))
(and (= (point-x a) (point-x b))
(= (point-y a) (point-y b))))
|
8.3 Object System Reference
|