A class describes a collection of objects
that share behavior. The objects described by a class
are called the instances of the class. The class
specifies the names of the slots that the
instance has, although it is up to the instance to
populate these slots with particular values.
The class also specifies the methods that can be
applied to its instances. Slot values can be anything,
but method values must be procedures.
Classes are hierarchical. Thus, a class can be a subclass of another class, which is called its superclass. A subclass not only has its own direct slots and methods, but also inherits all the
slots and methods of its superclass. If a class has a
slot or method that has the same name as its
superclass's, then the subclass's slot or method is the
one that is retained.
Let us now implement a basic object system in Scheme.
We will allow only one superclass per class (single inheritance). If we don't want to specify a
superclass, we will use #t as a ``zero''
superclass, one that has neither slots nor methods.
The superclass of #t is deemed to be itself.
As a first approximation, it is useful to define
classes using a struct called standard-class, with
fields for the slot names, the superclass, and the
methods. The first two fields we will call slots
and superclass respectively. We will use two
fields for methods, a
method-names field that will hold the list of names
of the class's methods, and a
method-vector field that will hold the vector of
the values of the class's methods.5
Here is the definition of the standard-class:
This is a very simple class. More complex classes
will have non-trivial superclasses and methods, which
will require a lot of standard initialization that we
would like to hide within the class creation process.
We will therefore define a macro called
create-class that will make the appropriate call to
make-standard-class.
We will defer the definition of the
create-class-proc procedure to later.
The procedure make-instance creates an instance of a class by generating a fresh vector based
on information enshrined in the class. The format of
the instance vector is very simple: Its first element
will refer to the class, and its remaining elements
will be slot values. make-instance's arguments are
the class followed by a sequence of twosomes, where
each twosome is a slot name and the value it assumes in
the instance.
(definemake-instance
(lambda (class . slot-value-twosomes)
;Find `n', the number of slots in `class'.;Create an instance vector of length `n + 1',;because we need one extra element in the instance;to contain the class.
(let* ((slotlist (standard-class.slotsclass))
(n (lengthslotlist))
(instance (make-vector (+n1))))
(vector-set!instance0class)
;Fill each of the slots in the instance;with the value as specified in the call to;`make-instance'.
(letloop ((slot-value-twosomesslot-value-twosomes))
(if (null?slot-value-twosomes) instance
(let ((k (list-position (carslot-value-twosomes)
slotlist)))
(vector-set!instance (+k1)
(cadrslot-value-twosomes))
(loop (cddrslot-value-twosomes))))))))
This assumes that class-of's argument will be a class
instance, ie, a vector whose first element points to some
instantiation of the standard-class.
We probably want to make class-of return an appropriate value
for any kind of Scheme object we feed to it.
We are now ready to tackle the definition of
create-class-proc. This procedure takes a
superclass, a list of slots, a list of method names,
and a vector of methods and makes the appropriate call
to make-standard-class. The only tricky part is
the value to be given to the slots field. It can't
be just the slots argument supplied via
create-class, for a class must include the slots of
its superclass as well. We must append the supplied
slots to the superclass's slots, making sure that we
don't have duplicate slots.
The procedure delete-duplicates called on a list
s, returns a new list that only includes the last
occurrence of each element of s.
(definedelete-duplicates
(lambda (s)
(if (null?s) s
(let ((a (cars)) (d (cdrs)))
(if (memvad) (delete-duplicatesd)
(consa (delete-duplicatesd)))))))
Now to the application of methods. We invoke the
method on an instance by using the procedure send.
send's arguments are the method name, followed by
the instance, followed by any arguments the method has
in addition to the instance itself. Since methods are
stored in the instance's class instead of the instance
itself, send will search the instance's class for
the method. If the method is not found there, it is
looked for in the class's superclass, and so on further
up the superclass chain:
Here, bike-class includes a method check-fit, that
takes a bike and an inseam measurement and reports on
the fit of the bike for a person of that inseam.
Let's redefine my-bike:
(definemy-bike
(make-instancebike-class'frame'titanium; I wish'size21'parts'ultegra'chain'sachs'tires'continental))
It cannot have escaped the astute reader that classes
themselves look like they could be the instances of
some class (a metaclass, if you will). Note that
all classes have some common behavior: each of them has
slots, a superclass, a list of method names, and a
method vector. make-instance looks like it could
be their shared method. This suggests that we could
specify this common behavior by another class (which
itself should, of course, be a class instance too).
In concrete terms, we could rewrite our class
implementation to itself make use of the
object-oriented approach, provided we make sure we
don't run into chicken-and-egg problems. In effect, we
will be getting rid of the class struct and its
attendant procedures and rely on the rest of the
machinery to define classes as objects.
Let us identify standard-class as the class of
which other classes are instances of. In particular,
standard-class must be an instance of itself. What
should standard-class look like?
We know standard-class is an instance, and we are
representing instances by vectors. So it is a
vector whose first element holds its class, ie,
itself, and whose remaining elements are slot values.
We have identified four slots that all classes must
have, so standard-class is a 5-element vector.
Note that the standard-class vector is
incompletely filled in: the symbol
value-of-standard-class-goes-here functions as a
placeholder. Now that we have defined a
standard-class value, we can use it to identify its
own class, which is itself:
(vector-set!standard-class0standard-class)
Note that we cannot rely on procedures based on the
class struct anymore. We should replace all calls
of the form
It is easy to modify the object system to allow classes
to have more than one superclass. We redefine the
standard-class to have a slot called
class-precedence-list instead of superclass.
The class-precedence-list of a class is the list of
all its superclasses, not just the direct
superclasses specified during the creation of the class
with create-class. The name implies that the
superclasses are listed in a particular order, where
superclasses occurring toward the front of the list
have precedence over the ones in the back of the list.
Not only has the list of slots changed to include
the new slot, but the erstwhile superclass slot is
now () instead of #t. This is because the
class-precedence-list of standard-class must be
a list. We could have had its value be (#t), but
we will not mention the zero class since it is in every
class's class-precedence-list.
The create-class macro has to modified to accept
a list of direct superclasses instead of a solitary superclass:
The create-class-proc must calculate the class precedence list from
the supplied direct superclasses, and the slot list from the
class precedence list:
5 We could in theory
define methods also as slots (whose values happen to be
procedures), but there is a good reason not to. The
instances of a class share methods but in general
differ in their slot values. In other words, methods
can be included in the class definition and don't have
to be allocated per instance as slots have to be.