In this section, a class in Gauche is explained in detail.
• Defining class: | ||
• Inheritance: | ||
• Class object: | ||
• Slot definition object: | ||
• Class redefinition: | ||
• Class definition examples: |
To define a class, use a macro define-class
.
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.
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.
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.
Returns the name of class.
(class-name <string>) ⇒ <string>
Returns the class precedence list of class.
(class-precedence-list <string>) ⇒ (#<class <string>> #<class <sequence>> #<class <collection>> #<class <top>>)
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>>)
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>
.
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))
.
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.
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.
Returns a list of methods that has class in its specializer.
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.
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.
Returns the name of a slot given by a slot definition object slot-def.
Returns a keyword-value list of slot options of slot-def.
Returns the value of :allocation
option of slot-def.
Returns the value of :getter
, :setter
and :accessor
slot options of slot-def, respectively.
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.
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:
define-class
.
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.
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.
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.
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.