|[ < ]||[ > ]||[ << ]||[ Up ]||[ >> ]||[Top]||[Contents]||[Index]||[ ? ]|
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
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
examples/oointro.scm of Gauche’s source distribution.)
<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
> is just a
convention; you can pass any symbol to
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
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
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
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
(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
defines the default value of the slot. The keyword after
defines the keyword argument which can be passed 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
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
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 grance, 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
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
three arguments, pt, dx, dy, where pt is
an instance of
(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
<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
reveals that it is an instance of
<generic>, a generic function.
describe truncates the printed value in
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).
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
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.
display function call a generic
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
(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
you can compare the instances by
(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
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
initialize method. The
is called with two arguments, one is a newly created instance,
and another is a list of arguments passed to
We define a
initialize method for
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,
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
And it is
<object>’s initialize method which takes care
of slot initialization. After calling
initialize method, you can assume all
the slots are properly initialized. So it is generally the
first thing in your
initialize method to call
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
method you just defined. In it, you call
which in turn calls
method. It initializes the instance with init-values and init-keywords.
After it returns, you register the new
to the global shape list
<shape> class represents just an abstract concept of
shape. Now we define some concrete drawable shapes, by
(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
It indicates that
inherit slots of
<shape> class, and also instances of
those subclasses can be accepted wherever an instance of
<shape> class is accepted.
<point-shape> adds one slot,
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
There appeared a new slot option in
:init-form slot option specifies the default value of
the slot when init-keyword is not given to
: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
<2d-point> instance is only created if the
<point-shape> instance is created without having
A shape may be drawn in different formats for different devices.
For now, we just consider a PostScript output. To make the
method polymorphic, we define a postscript output device class,
(define-class <ps-device> () ())
Then we can write a
draw method, specialized for
(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
draw method that is specialized for such devices.
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
(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 srfi-1) ;; for iota (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>))) )
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-sampe)
|[ < ]||[ > ]||[ << ]||[ Up ]||[ >> ]|
This document was generated on July 19, 2014 using texi2html 1.82.