For Development HEAD DRAFTSearch (procedure/syntax/module):

7.2 Class

In this section, a class in Gauche is explained in detail.


7.2.1 Defining class

To define a class, use a macro define-class.

Macro: define-class name supers (slot-spec …) option …

Creates a class object according to the arguments, and globally bind it to a variable name. This macro should be used at toplevel.

Supers is a list of direct superclasses from which this class inherits. You can use multiple inheritance. All Scheme-defined classes implicitly inherits <object>. It is implicitly added to the right of supers list, so you don’t need to specify it. See Inheritance, for the details about inheritance.

Slot-spec is a specification of a "slot", sometimes known as a "field" or an "instance variable" (but you can specify "class variable" in slot-spec as well). The simplest form of slot-spec is just a symbol, which names the slot. Or you can give a list, whose first element is a symbol and whose rest is an interleaved list of keywords and values. The list form not only defines a name of the slot but specifies behavior of the slot. It is explained below.

Finally, option … is an interleaved list of keywords and values, specifies how class object should be created. This macro recognizes one keyword, :metaclass, whose corresponding value is used for metaclass (class that instantiates another class). Other options are passed to the make method to create the class object. See Class instantiation, for the usage of metaclass.

If a slot specification is a list, it should be in the following form:

(slot-name :option1 value1 :option2 value2 ...)

Each keyword (option1 etc.) gives a slot option. By default, the following slot options are recognized. You can add more slot options by defining metaclass.

:allocation

Specifies an allocation type of this slot, which specifies how the value for this slot is stored. The following keyword values are recognized by the standard class. A programmer can define his own metaclass to extend the class to recognize other allocation types.

:instance

A slot is allocated for each instance, so that every instance can have distinct value. This realizes so-called "instance variable" behavior. If :allocation slot option is omitted, this is the default.

:class

A slot is allocated in this class object, so that every instance will share the same value for this slot. This realizes so-called "class variable" behavior. The slot value is also shared by all subclasses (unless a subclass definition shadows the slot).

:each-subclass

Similar to class allocation, but a slot is allocated for each class; that is, it is shared by every instance of the class, but not shared by the instances of its subclasses.

:virtual

No storage is allocated for this type of slot. Accessing the slot calls procedures given in :slot-ref and :slot-set! options described below. In other words, you can make a procedural slot. If a slot’s allocation is specified as virtual, at least :slot-ref option has to be specified as well, or define-class raises an error.

:builtin

This type of allocation only appears in built-in classes, and you can’t specify it in Scheme-defined class.

:init-keyword

A keyword value given to this slot option can be used to pass an initial value to make method when an instance is created.

:init-value

Gives an initial value of the slot, if the slot is not initialized by the keyword argument at the creation time. The value is evaluated when define-class is evaluated.

:init-form

Like init-value, but the value given is wrapped in a thunk, and evaluated each time when the value is required. If both init-value and init-form are given, init-form is ignored. Actually, :init-form expr is converted to :init-thunk (lambda () expr) by define-class macro.

:initform

A synonym of init-form. This is kept for compatibility to STk, and shouldn’t be used in the new code.

:init-thunk

Gives a thunk, which will be evaluated to obtain an initial value of the slot, if the slot is not initialized by the keyword argument at the creation time. To give a value to :init-form is equivalent to give (lambda () value) to :init-thunk.

:immutable

If not false, this slot is immutable. To be precise, you can set the slot value only once, which usually happens at instance initializaiton. The slot setter raises an error unless the slot is previously unbound. If the slot is left unbound by initialize method, it can be set later. It is regarded as a delayed initialization.

:getter

Takes a symbol, and a getter method is created and bound to the generic function of that name. The getter method takes an instance of the class and returns the value of the slot.

:setter

Takes a symbol, and a setter method is created and bound to the generic function of that name. The setter method takes an instance of the class and a value, and sets the value to the slot of the instance.

:accessor

Takes a symbol, and create two methods; a getter method and a setter method. A getter method is bound to the generic function of the given name, and a setter method is added as the setter of that generic function (see Assignments for generic setters).

:slot-ref

Specifies a value that evaluates to a procedure which takes one argument, an instance. This slot option must be specified if the allocation of the slot is virtual. Whenever a program tries to get the value of the slot, either using slot-ref or the getter method, the specified procedure is called, and its result is returned as the value of the slot. The procedure can return an undef value (the return value of undefined) to indicate the slot doesn’t have a value. If the slot allocation is not virtual this slot option is ignored.

:slot-set!

Specifies a value that evaluates to a procedure which takes two arguments, an instance and a value. Whenever a program tries to set the value of the slot, either using slot-set! or the setter method, the specified procedure is called with the value to be set. If the slot allocation is not virtual this slot option is ignored. If this option isn’t given to a virtual slot, the slot becomes read-only.

:slot-bound?

Specifies a value that evaluates to a procedure which takes one argument, an instance. This slot option is only meaningful when the slot allocation is virtual. Whenever a program tries to determine whether the slot has a value, this procedure is called. It should return a true value if the slot has a value, or #f otherwise. If this slot option is omitted for a virtual slot, the system calls the procedure given to slot-ref instead, and see whether its return value is #<undef> or not.


7.2.2 Inheritance

Inheritance has two roles. First, you can extend the existing class by adding more slots. Second, you can specialize the methods related to the existing class so that those methods will do a little more specific task than the original methods.

Let’s define some terms. When a class <T> inherits a class <S>, we call <T> a subclass of <S>, and <S> a superclass of <T>. This relation is transitive: <T>’s subclasses are also <S>’s subclasses, and <S>’s superclasses are also <T>’s superclasses. Specifically, if <T> directly inherits <S>, that is, <S> appeared in the superclass list when <T> is defined, then <S> is a direct superclass of <T>, and <T> is a direct subclass of <S>.

When a class is defined, it and its superclasses are ordered from subclasses to superclasses, and a list of classes is created in such order. It is called class precedence list, or CPL. Every class has its own CPL. A CPL of a class always begins with the class itself, and ends with <top>.

You can query a class’s CPL by a procedure class-precedence-list:

gosh> (class-precedence-list <boolean>)
(#<class <boolean>> #<class <top>>)
gosh> (class-precedence-list <string>)
(#<class <string>> #<class <sequence>> #<class <collection>> #<class <top>>)

As you see, all classes inherits a class named <top>. Some built-in classes have several abstract classes in its CPL between itself and <top>; the above example shows <string> class inherits <sequence> and <collection>. That means a string can behave both as a sequence and a collection.

gosh> (is-a? "abc" <string>)
#t
gosh> (is-a? "abc" <sequence>)
#t
gosh> (is-a? "abc" <collection>)
#t

How about inheritance of Scheme-defined classes? If there’s only single inheritance, its CPL is straightforward: you can just follow the class’s super, its super’s super, its super’s super’s super, …, until you reach <top>. See the example:

gosh> (define-class <a> () ())
<a>
gosh> (define-class <b> (<a>) ())
<b>
gosh> (class-precedence-list <b>)
(#<class <b>> #<class <a>> #<class <object>> #<class <top>>)

Scheme-defined class always inherits <object>. It is automatically inserted by the system.

When multiple inheritance is involved, a story becomes a bit complicated. We have to merge multiple CPLs of the superclasses into one CPL. It is called linearization, and there are several known linearization strategies. By default, Gauche uses an algorithm called C3 linearization, which is consistent with the local precedence order, monotonicity, and the extended precedence graph. We don’t go into the details here; as a general rule, the order of superclasses in a class’s CPL is always consistent to the order of direct superclasses of the class, the order of CPL of each superclasses, and the order of direct superclasses of each superclass, and so on. For the precise description, see Kim Barrett, Bob Cassels, Paul Haahr, David A. Moon, Keith Playford, P. Tucker Withington, A Monotonic Superclass Linearization for Dylan, in Proceedings of OOPSLA 96, October 1996.

If a class inherits superclasses in a way that its CPL can’t be constructed with satisfying consistencies, an error is reported.

Here’s a simple example of multiple inheritance.

(define-class <grid-layout> () ())

(define-class <horizontal-grid> (<grid-layout>) ())

(define-class <vertical-grid> (<grid-layout>) ())

(define-class <hv-grid> (<horizontal-grid> <vertical-grid>) ())

(map class-name (class-precedence-list <hv-grid>))
 ⇒ (<hv-grid> <horizontal-grid> <vertical-grid>
     <grid-layout> <object> <top>)

Note that the order of direct superclasses of <hv-grid> (<horizontal-grid> and <vertical-grid>) is kept.

The following is a little twisted example:

(define-class <pane> () ())

(define-class <scrolling-mixin> () ())

(define-class <scrollable-pane> (<pane> <scrolling-mixin>) ())

(define-class <editing-mixin> () ())

(define-class <editable-pane> (<pane> <editing-mixin>) ())

(define-class <editable-scrollable-pane>
   (<scrollable-pane> <editable-pane>) ())

(map class-name (class-precedence-list <editable-scrollable-pane>))
 ⇒ (<editable-scrollable-pane> <scrollable-pane>
     <editable-pane> <pane> <scrolling-mixin> <editing-mixin>
     <object> <top>)

Once the class precedence order is determined, the slots of defined class is calculated as follows: the slot definitions are collected in the direction from superclasss to subclass in CPL. If a subclass has a slot definition of the same name of the one in superclass, then the slot definition of the subclass is taken and superclass’s is discarded. Suppose a class <S> defines slots a, b, and c, a class <T> defines slots c, d, and e, and a class <U> defines slots b and e. When <U>’s CPL is (<U> <T> <S> <object> <top>), then <U>’s slots is calculated as the chart below; that is, <U> gets five slots, of which b and e’s definitions come from <U>’s definitions, c and d’s come from <T>, and a’s comes from <S>.

   CPL      | slot definitions
            |  () indicates shadowed slot
 -----------+-------------------
   <top>    |
   <object> |
   <S>      | a  (b) (c)
   <T>      |         c   d  (e)
   <U>      |     b           e
 -----------+--------------------
 <U>'s slots| a   b   c   d   e

You can get a list of slot definitions of a class object using class-slots function.

Note that the behavior described above is mere a default behavior. You can customize how the CPL is computed, or how slot definitions are inherited, by defining metaclass. For example, you can write a metaclass that allows you to merge slot options of the same slot names, instead of the one shadowing the other. Or you can write a metaclass that forbids a subclass shadows the superclass’s slot.


7.2.3 Class object

What is a class? In Gauche, a class is just an object that implements a specific feature: to instantiate an object. Because of that, you can introspect the class by just looking into the slot values. There are some procedures provided for the convenience of such introspection. Note that if those procedures return a list, it belongs to the class and you shouldn’t modify it.

Function: class-name class

Returns the name of class.

(class-name <string>) ⇒ <string>
Function: class-precedence-list class

Returns the class precedence list of class.

(class-precedence-list <string>)
  ⇒ (#<class <string>>
      #<class <sequence>>
      #<class <collection>>
      #<class <top>>)
Function: class-direct-supers class

Returns a list of direct superclasses of class. A direct superclass is a class from which class inherits directly.

(class-direct-supers <string>)
  ⇒ (#<class <sequence>>)
Function: class-direct-subclasses class

Returns a list of direct subclasses of class. A direct subclass is a class that directly inherits class. If <T> is a direct subclass of <S>, then <S> is a direct superclass of <T>.

Function: class-slots class

Returns a list of slot definitions of class. A slot definition is a list whose car is the name of the slot and whose cdr is a keyword-value list that specifies slot options. You can further inspect a slot definition to know what characteristics the slot has. See Slot definition object for the details.

The standard way to get a list of slot names of a given class is (map slot-definition-name (class-slots class)).

Function: class-slot-definition class slot-name

Returns a slot definition of a slot specified by slot-name in a class class. If class doesn’t have a named slot, #f is returned.

Function: class-direct-slots class

Returns a list of slot definitions that are directly defined in this class (i.e. not inherited from superclasses). This information is used to calculate slot inheritance during class initialization.

Function: class-direct-methods class

Returns a list of methods that has class in its specializer.

Function: class-slot-accessor class slot-name

Returns a slot accessor object of the slot specified by slot-name in class. A slot accessor object is an internal object that encapsulates the information how to access, modify, and initialize the given slot.

You don’t usually need to deal with slot accessor objects unless you are defining some special slots using metaobject protocol.


7.2.4 Slot definition object

A slot definition object, returned by class-slots, class-direct-slots and class-slot-definition, keeps information about a slot. Currently Gauche uses a list to represent the slot definition, as STklos and TinyCLOS do. However, it is not guaranteed that Gauche keeps such a structure in future; you should use the following dedicated accessor methods to obtain information of a slot definition object.

Function: slot-definition-name slot-def

Returns the name of a slot given by a slot definition object slot-def.

Function: slot-definition-options slot-def

Returns a keyword-value list of slot options of slot-def.

Function: slot-definition-allocation slot-def

Returns the value of :allocation option of slot-def.

Function: slot-definition-getter slot-def
Function: slot-definition-setter slot-def
Function: slot-definition-accessor slot-def

Returns the value of :getter, :setter and :accessor slot options of slot-def, respectively.

Function: slot-definition-option slot-def option :optional default

Returns the value of slot option option of slot-def. If there’s no such an option, default is returned if given, or an error is signaled otherwise.


7.2.5 Class redefinition

If the specified class name is bound to a class when define-class is used, it is regarded as redefinition of the original class.

Redefinition of a class means the following operations:

Note that the original class and the new class are different objects. The original class object remembers which variable in which module it is originally bound, and replaces the binding to a new class. If you keep the direct reference to the original class somewhere else, it still refers to the original class; you might want to take extra care. You can customize class redefinition behavior by defining the class-redefinition method; see Metaobject protocol for the details.

If there are instances of the original class, such instances are automatically updated when it is about to be accessed or modified via class-of, is-a?, slot-ref, slot-set!, ref, a getter method, or a setter method.

Updating an instance means that the class of the instance is changed (from the old class to the new class). By default, the values of the slots that are common in the original class and the new class are carried over, and the slots added by the new class are initialized according to the slot specification of the new class, and the values of the slots that are removed from the original class are discarded. You can customize this behavior by writing the change-class method. See Changing classes, for the details.

Notes on thread safety

Class redefinition process is non-local operation with full of side-effects. It is difficult to guarantee that two threads safely run class redefinition protocol simultaneously. So Gauche uses a process-wide lock to limit only one thread to enter the class redefinition protocol at a time.

If a thread tries to redefine a class while another thread is in the redefinition protocol, the thread is blocked, even if it is redefining a class different from the one that are being redefined; because redefinition affects all the subclasses, and all the methods and generic functions that are related to the class and subclasses, it is not trivial to determine two classes are completely independent or not.

If a thread tries to access an instance whose class is being redefined by another thread, also the thread is blocked until the redefinition is finished.

Note that the instance update protocol isn’t serialized. If two threads try to access an instance whose class has been redefined, both trigger the instance update protocol, which would cause an undesired race condition. It is the application’s responsibility to ensure such a case won’t happen. It is natural since the instance access isn’t serialized by the system anyway. However, an extra care is required to have mutex within an instance; just accessing the mutex in it may trigger the instance update protocol.

Notes on compatibility

Class redefinition protocols subtlety differ among CLOS-like Scheme systems. Gauche’s is very similar to STklos’s, except that STklos 0.56 doesn’t replace bindings of redefined subclasses, and also it doesn’t remember initialization arguments so the redefined subclass may lose some of the information that the original subclass has. Guile’s object system swaps identities of the original class and the redefined class at the end of class redefinition protocol, so the reference to the original class object will turn to the redefined class. As far as the author knows, class redefinition is not thread-safe in both STklos 0.56 and Guile 1.6.4.


7.2.6 Class definition examples

Let’s see some examples. Suppose you are defining a graphical toolkit. A <window> is a rectangle region on the screen, so it has width and height. It can be organized hierarchically, i.e. a window can be placed within another window; so it has a pointer to the parent window. And we specify the window’s position, x, y, by the coordinate relative to its parent window. Finally, we create a "root" window that covers entire screen. It also serves the default parent window. So far, what we get is something like this:

;; The first version
(define-class <window> ()
  (;; Pointer to the parent window.
   (parent      :init-keyword :parent :init-form *root-window*)
   ;; Sizes of the window
   (width       :init-keyword :width  :init-value 1)
   (height      :init-keyword :height :init-value 1)
   ;; Position of the window relative to the parent.
   (x           :init-keyword :x :init-value 0)
   (y           :init-keyword :y :init-value 0)
   ))

(define *screen-width* 1280)
(define *screen-height* 1024)

(define *root-window*
  (make <window> :parent #f :width *screen-width* :height *screen-height*))

Note the usage of :init-value and :init-form. When the <window> class is defined, we haven’t bound *root-window* yet, so we can’t use :init-value here.

gosh> *root-window*
#<<window> 0x80db1d0>
gosh> (define window-a (make <window> :width 100 :height 100))
window-a
gosh> (d window-a)
#<<window> 0x80db1b0> is an instance of class <window>
slots:
  parent    : #<<window> 0x80db1d0>
  width     : 100
  height    : 100
  x         : 0
  y         : 0
gosh> (define window-b
        (make <window> :parent window-a :width 50 :height 20 :x 10 :y 5))
window-b
gosh> (d window-b)
#<<window> 0x80db140> is an instance of class <window>
slots:
  parent    : #<<window> 0x80db1b0>
  width     : 50
  height    : 20
  x         : 10
  y         : 5

If you’re like me, you don’t want to expose a global variable such as *root-window* for users of your toolkit. One way to encapsulate it (to certain extent) is to keep the pointer to the root window in a class variable. Add the following slot option to the definition of <window>, and the slot root-window of the <window> class refers to the same storage space.

(define-class <window> ()
  (...
   ...
   (root-window :allocation :class)
   ...))

You can use slot-ref and slot-set! on an instance of <window>, or use class-slot-ref and class-slot-set! on the <window> class itself, to get/set the value of the root-window slot.

The users of the toolkit may want to get the absolute position of the window (the coordinates in the root window) instead of the relative position. You may provide virtual slots that returns the absolute positions, like the following:

(define-class <window> ()
  (...
   ...
   (root-x :allocation :virtual
           :slot-ref  (lambda (o)
                        (if (ref o 'parent)
                            (+ (ref (ref o 'parent) 'root-x)
                               (ref o 'x))
                            (ref o 'x)))
           :slot-set! (lambda (o v)
                        (set! (ref o 'x)
                              (if (ref o 'parent)
                                  (- v (ref (ref o 'parent) 'root-x))
                                  v)))
            )
    ...))

Whether providing such interface via methods or virtual slots is somewhat a matter of taste. Using virtual slots has an advantage of being able to hide the change of implementation, i.e. you can change to keep root-x in a real slot and make x a virtual slot later without breaking the code using <window>. (In the mainstream object-oriented languages, such kind of "hiding implementation" is usually achieved by hiding instance variables and exposing methods. In Gauche and other CLOS-like systems, slots are always visible to the users, so the situation is a bit different.



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT