STklos Reference Manual
8. STklos Object System


Contents

8.1 Introduction

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) [8], in the spirit of the one defined for CLOS [9].

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 [7]. 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 [9]. Some features of STklos are also issued from Dylan [3] or SOS [5].

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 [9] or Dylan [3]. 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

8.2.1.1 Class definition

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:

(describe c)

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
8.2.1.3 Slot Definition

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:

(x-of p)

and setting it to 100 can be done using the extended set!

(set! (x-of p) 100)

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
8.2.1.4 Virtual Slots

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   ; (i.e. 4 * Pi)
(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 Inheritance

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 Generic function

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

  1. 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),
  2. sorts the list of applicable methods in the "most specific" order,
  3. 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:

  1. Before defining a new generic functiondefine-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)  ; transform "log" in a generic function
    (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 ; standard "log" procedure
    
  2. 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.

8.2.3.2 Next-method

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

8.3.1 Class Definition




1: This section is an adaptation of Jeff Dalton's (J.Dalton@ed.ac.uk) "Brief introduction to CLOS" which can be found at the URL http://www.aiai.ed.ac.uk/~jeff/clos-guide.html
2: We suppose here that we are in a context where
(define-reader-ctor 'make make)
as already been evaluated

This Html page has been produced by Skribe.
Last update Sat Dec 31 15:49:34 2011