This section briefly explains the basic structure of Gauche’s object system. It is strongly influenced by CLOS (Common-Lisp Object System). If you have experience in CLOS or related systems such as TinyCLOS, STklos or Guile’s object system, you may skip to the next section.
Three concepts play the central role in CLOS-like object systems: A class, a generic function, and a method.
A class specifies a structure of object. It also defines a datatype (strictly speaking, it’s not the same thing as a datatype, but let’s skip the complicated part for now).
For example, a point in 2D space can be represented by
x and y coordinates.
A point class can be defined using define-class
macro.
In the shortest form, it can be defined like this:
(define-class <2d-point> () (x y))
(You can find the code of definitions in the examples of this section
in examples/oointro.scm
of Gauche’s source distribution.)
The symbol <2d-point>
is the name of the class, and also
the global variable <2d-point>
is bound to a class object.
Surrounding a class name by <
and >
is just a
convention; you can pass any symbol to define-class
.
The second argument of define-class
is a list of
direct superclasses, which specifies inheritance of the class.
We’ll come back to it later.
The third argument of define-class
is a list of
slots. A slot is a storage space, usually in each object,
where you can store a value. It is something similar to
what is called a field or an instance variable in other
object-oriented languages; but slots can be configured more
than just a per-object storage space.
Now we defined a 2D point class, so we can create an instance
of a point. You can pass a class to a generic function make
to create an instance. (Don’t worry about what generic function
is—think it as a special type of function, just for now).
(define a-point (make <2d-point>)) a-point ⇒ #<<2d-point> 0x8117570>
If you are using gosh
interactively, you can use
a generic function describe
to inspect the internal
of an instance.
A short alias, d
, is defined to describe
for
the convenience. (See gauche.interactive
- Utilities for interactive session
for the details).
gosh> (d a-point) #<<2d-point> 0x8117570> is an instance of class <2d-point> slots: x : #<unbound> y : #<unbound>
In order to access or modify the value of the slot, you can use
slot-ref
and slot-set!
, respectively.
These names are taken from STklos.
(slot-ref a-point 'x) ;; access to the slot x of a-point
⇒ error, since slot ’x doesn’t have a value yet
(slot-set! a-point 'x 10.0) ;; set 10.0 to the slot x of a-point
(slot-ref a-point 'x)
⇒ 10.0
Gauche also provides a shorter name, ref
, which can also
be used in SRFI-17’s generalized set!
syntax:
(ref a-point 'x) ⇒ 10.0 (set! (ref a-point 'y) 20.0) (ref a-point 'y) ⇒ 20.0
Now you can see slot values are set.
gosh> (d a-point) #<<2d-point> 0x8117570> is an instance of class <2d-point> slots: x : 10.0 y : 20.0
In practice, it is usually convenient if you can specify the default
value for a slot, or give values for slots when you create an instance.
Such information can be specified by slot options.
Let’s modify the definition of <2d-point>
like this:
(define-class <2d-point> () ((x :init-value 0.0 :init-keyword :x :accessor x-of) (y :init-value 0.0 :init-keyword :y :accessor y-of)))
Note that each slot specification is now a list, instead of just
a symbol as in the previous example.
The list’s car now specifies the slot name, and its cdr
gives various information. The value after :init-value
defines the default value of the slot. The keyword after :init-keyword
defines the keyword argument which can be passed to make
to
initialize the slot at creation time.
The name after keyword :accessor
is bound to a generic
function that can be used to access/modify the slot, instead of
using slot-ref
/slot-set!
.
Let’s see some interactive session. You create an instance
of the new <2d-point>
class, and you can see the slots are
initialized by the default values.
gosh> (define a-point (make <2d-point>)) a-point gosh> (d a-point) #<<2d-point> 0x8148680> is an instance of class <2d-point> slots: x : 0.0 y : 0.0
You create another instance, this time giving initialization values by keyword arguments.
gosh> (define b-point (make <2d-point> :x 50.0 :y -10.0)) b-point gosh> (d b-point) #<<2d-point> 0x8155b80> is an instance of class <2d-point> slots: x : 50.0 y : -10.0
Accessors are less verbose than slot-ref
/slot-set!
, thus
convenient.
gosh> (x-of a-point) 0.0 gosh> (x-of b-point) 50.0 gosh> (set! (y-of a-point) 3.33) #<undef> gosh> (y-of a-point) 3.33
The full list of available slot options is described in Defining class. At a first glance, the declarations of such slot options may look verbose. The system might have provide a static way to define init-keywords or accessor names automatically; however, CLOS-like systems prefer flexibility. Using a mechanism called metaobject protocol, you can customize how these slot options are interpreted, and you can add your own slot options as well. See Metaobject protocol, for details.
We can also have <2d-vector>
class in similar fashion.
(define-class <2d-vector> () ((x :init-value 0.0 :init-keyword :x :accessor x-of) (y :init-value 0.0 :init-keyword :y :accessor y-of)))
Yes, we can use the same accessor name like x-of
, and
it is effectively overloaded.
If you are familiar with mainstream object-oriented languages,
you may wonder where methods are. Here they are.
The following form defines a method move-by!
of
three arguments, pt, dx, dy, where pt is
an instance of <2d-point>
.
(define-method move-by! ((pt <2d-point>) dx dy) (inc! (x-of pt) dx) (inc! (y-of pt) dy))
The second argument of define-method
macro specifies a
method specializer list. It indicates the first argument must be
an instance of <2d-point>
, and the second and third
can be any type. The syntax to call a method is just like
the one to call an ordinary function.
gosh> (move-by! b-point 1.4 2.5) #<undef> gosh> (d b-point) #<<2d-point> 0x8155b80> is an instance of class <2d-point> slots: x : 51.4 y : -7.5
You can overload the method by different specializers; here you can move a point using a vector.
(define-method move-by! ((pt <2d-point>) (delta <2d-vector>)) (move-by! pt (x-of delta) (y-of delta)))
Specialization isn’t limited to a user-defined classes. You can also specialize a method using Gauche’s built-in type.
(define-method move-by! ((pt <2d-point>) (c <complex>)) (move-by! pt (real-part c) (imag-part c)))
And here’s the example session:
gosh> (define d-vector (make <2d-vector> :x -9.0 :y 7.25)) d-vector gosh> (move-by! b-point d-vector) #<undef> gosh> (d b-point) #<<2d-point> 0x8155b80> is an instance of class <2d-point> slots: x : 42.4 y : -0.25 gosh> (move-by! b-point 3+2i) #<undef> gosh> (d b-point) #<<2d-point> 0x8155b80> is an instance of class <2d-point> slots: x : 45.4 y : -2.25
You see that a method is dispatched not only by its primary
receiver (<2d-point>
), but also other arguments.
In fact, the first argument is no more special than the rest.
In CLOS-like system a method does not belong to
a particular class.
So what is actually a method? Inspecting move-by!
reveals that it is an instance of <generic>
, a generic function.
(Note that describe
truncates the printed value in methods
slot for the sake of readability).
gosh> move-by! #<generic move-by! (3)> gosh> (d move-by!) #<generic move-by! (3)> is an instance of class <generic> slots: name : move-by! methods : (#<method (move-by! <2d-point> <complex>)> #<method (move- gosh> (ref move-by! 'methods) (#<method (move-by! <2d-point> <complex>)> #<method (move-by! <2d-point> <2d-vector>)> #<method (move-by! <2d-point> <top> <top>)>)
I said a generic function is a special type of function. It is recognized by Gauche as an applicable object, but when applied, it selects appropriate method(s) according to its arguments and calls the selected method(s).
What the define-method
macro actually does is (1) to create
a generic function of the given name if it does not exist yet,
(2) to create a method object with the given specializers
and the body, and (3) to add the method object to the generic function.
The accessors are also generic functions, created implicitly by the
define-class
macro.
gosh> (d x-of) #<generic x-of (2)> is an instance of class <generic> slots: name : x-of methods : (#<method (x-of <2d-vector>)> #<method (x-of <2d-point>)>)
In the mainstream dynamic object-oriented languages, a class has many roles; it defines a structure and a type, creates a namespace for its slots and methods, and is responsible for method dispatch. In Gauche, namespace is managed by modules, and method dispatch is handled by generic functions.
The default printed representation of object is not very user-friendly.
Gauche’s write
and display
function call a generic
function write-object
when they encounter an instance
they don’t know how to print. You can define its method
specialized to your class to customize how the instance is
printed.
(define-method write-object ((pt <2d-point>) port) (format port "[[~a, ~a]]" (x-of pt) (y-of pt))) (define-method write-object ((vec <2d-vector>) port) (format port "<<~a, ~a>>" (x-of vec) (y-of vec)))
And what you’ll get is:
gosh> a-point [[0.0, 3.33]] gosh> d-vector <<-9.0, 7.25>>
If you customize the printed representation to conform SRFI-10 format, and define a corresponding read-time constructor, you can make your instances to be written-out and read-back just like built-in objects. See Read-time constructor for the details.
Several built-in functions have similar way to extend their
functionality for user-defined objects. For example, if
you specialize a generic function object-equal?
,
you can compare the instances by equal?
:
(define-method object-equal? ((a <2d-point>) (b <2d-point>)) (and (equal? (x-of a) (x-of b)) (equal? (y-of a) (y-of b)))) (equal? (make <2d-point> :x 1 :y 2) (make <2d-point> :x 1 :y 2)) ⇒ #t (equal? (make <2d-point> :x 1 :y 2) (make <2d-point> :x 2 :y 1)) ⇒ #f (equal? (make <2d-point> :x 1 :y 2) 'a) ⇒ #f (equal? (list (make <2d-point> :x 1 :y 2) (make <2d-point> :x 3 :y 4)) (list (make <2d-point> :x 1 :y 2) (make <2d-point> :x 3 :y 4))) ⇒ #t
Let’s proceed to more interesting examples.
Think of a class <shape>
,
which is an entity that can be drawn.
As a base class, it keeps
common attributes such as a color and line thickness in its slots.
(define-class <shape> () ((color :init-value '(0 0 0) :init-keyword :color) (thickness :init-value 2 init-keyword :thickness)))
When an instance is created, make
calls a generic function
initialize
, which takes care of initializing slots
such as processing init-keywords and init-values.
You can customize the initialization behavior by specializing
the initialize
method. The initialize
method
is called with two arguments, one is a newly created instance,
and another is a list of arguments passed to make
.
We define a initialize
method for <shape>
class,
so that the created shape will be automatically recorded in a global
list. Note that we don’t want to replace system’s
initialize
behavior completely,
since we still need the init-keywords to be handled.
(define *shapes* '()) ;; global shape list (define-method initialize ((self <shape>) initargs) (next-method) ;; let the system to handle slot initialization (push! *shapes* self)) ;; record myself to the global list
The trick is a special method, next-method
. It can only be
used inside a method body, and calls less specific method
of the same generic function—typically, it means you call the
same method of superclass.
Most object-oriented languages have the concept of calling
superclass’s method. Because of multiple-argument
dispatching and multiple inheritance, next-method
is
a little bit more complicated, but the basic idea is the same.
So, what’s the superclass of <shape>
? In fact, all
Scheme-defined class inherits a class called <object>
.
And it is <object>
’s initialize method which takes care
of slot initialization. After calling next-method
within your initialize
method, you can assume all
the slots are properly initialized. So it is generally the
first thing in your initialize
method to call next-method
.
Let’s inspect the above code. When you call
(make <shape> args …)
, the system allocates
memory for an instance of <shape>
, and calls
initialize
generic function with the instance and
args …
. It is dispatched to the initialize
method you just defined. In it, you call next-method
,
which in turn calls <object>
class’s initialize
method. It initializes the instance with init-values and init-keywords.
After it returns, you register the new <shape>
instance
to the global shape list *shapes*
.
The <shape>
class represents just an abstract concept of
shape. Now we define some concrete drawable shapes, by
subclassing the <shape>
class.
(define-class <point-shape> (<shape>) ((point :init-form (make <2d-point>) :init-keyword :point))) (define-class <polyline-shape> (<shape>) ((points :init-value '() :init-keyword :points) (closed :init-value #f :init-keyword :closed)))
Note the second argument passed to define-class
.
It indicates that <point-shape>
and <polyline-shape>
inherit slots of <shape>
class, and also instances of
those subclasses can be accepted wherever an instance of
<shape>
class is accepted.
The <point-shape>
adds one slot, point
, which
contains an instance of <2d-point>
defined in the beginning
of this section. The <polyline-shape>
class stores
a list of points, and a flag, which specifies whether the end
point of the polyline is connected to its starting point or not.
Inheritance is a powerful mechanism that should be used with care,
or it easily result a code which is untractable
("Object-oriented programming offers a sustainable way to
write spaghetti code.", as Paul Graham says in his article
"The Hundred-Year Language").
The rule of thumb is to make a subclass when you need a subtype.
The inheritance of slots is just something that comes with,
but it shouldn’t be the main reason to do subclassing.
You can always "include" the substructure, as is done in
<point-shape>
class.
There appeared a new slot option in <point-shape>
class.
The :init-form
slot option specifies the default value of
the slot when init-keyword is not given to make
method.
However, unlike :init-value
, with which the value is
evaluated at the time the class is defined,
the value with :init-form
is evaluated when the system
actually needs the value. So, in the <point-shape>
instance,
the default <2d-point>
instance is only created if the
<point-shape>
instance is created without having :point
init-keyword argument.
A shape may be drawn in different formats for different devices.
For now, we just consider a PostScript output. To make the draw
method polymorphic, we define a postscript output device class,
<ps-device>
.
(define-class <ps-device> () ())
Then we can write a draw
method, specialized for
both <shape>
and <ps-device>
.
(define-method draw ((self <shape>) (device <ps-device>)) (format #t "gsave\n") (draw-path self device) (apply format #t "~a ~a ~a setrgbcolor\n" (ref self 'color)) (format #t "~a setlinewidth\n" (ref self 'thickness)) (format #t "stroke\n") (format #t "grestore\n"))
In this code, the device argument isn’t
used within the method body. It is just used for method dispatching.
If we eventually have different output devices, we can add
a draw
method that is specialized for such devices.
The above draw
method does the common work, but actual
drawing must be done in specialized way for each subclasses.
(define-method draw-path ((self <point-shape>) (device <ps-device>)) (apply format #t "newpath ~a ~a 1 0 360 arc closepath\n" (point->list (ref self 'point)))) (define-method draw-path ((self <polyline-shape>) (device <ps-device>)) (let ((pts (ref self 'points))) (when (>= (length pts) 2) (format #t "newpath\n") (apply format #t "~a ~a moveto\n" (point->list (car pts))) (for-each (lambda (pt) (apply format #t "~a ~a lineto\n" (point->list pt))) (cdr pts)) (when (ref self 'closed) (apply format #t "~a ~a lineto\n" (point->list (car pts)))) (format #t "closepath\n")))) ;; utility method (define-method point->list ((pt <2d-point>)) (list (x-of pt) (y-of pt)))
Finally, we do a little hack. Let draw
method work on
the list of shapes, so that we can draw multiple shapes within a page
in batch.
(define-method draw ((shapes <list>) (device <ps-device>)) (format #t "%%\n") (for-each (cut draw <> device) shapes) (format #t "showpage\n"))
Then we can write some simple figures ….
(use math.const) ;; for constant pi (define (shape-sample) ;; creates 5 corner points of pentagon (define (make-corners scale) (map (lambda (i) (let ((pt (make <2d-point>))) (move-by! pt (make-polar scale (* i 2/5 pi))) (move-by! pt 200 200) pt)) (iota 5))) (set! *shapes* '()) ;; clear the shape list (let* ((corners (make-corners 100))) ;; a pentagon in green (make <polyline-shape> :color '(0 1 0) :closed #t :points corners) ;; a star-shape in red (make <polyline-shape> :color '(1 0 0) :closed #t :points (list (list-ref corners 0) (list-ref corners 2) (list-ref corners 4) (list-ref corners 1) (list-ref corners 3))) ;; put dots in each corner of the star (for-each (cut make <point-shape> :point <>) (make-corners 90)) ;; draw the shapes (draw *shapes* (make <ps-device>))) )
The function shape-sample
writes out a PostScript code of
simple drawing to the current output port. You can write it out
to file by the following expression, and then view the result
by PostScript viewer such as GhostScript.
(with-output-to-file "oointro.ps" shape-sample)