ScmObj is an object system for Scheme that provides
classes with multiple inheritance;
methods that can specialize on one or more arguments (``multimethods'');
methods in addition to primary methods;
:around methods; and
standard method combination a la the default protocol in CLOS.
ScmObj lets you define a new class with the
make-class takes two subexpressions: (1) a
list of direct superclasses and (2) a list of
(define human-c (make-class () (:name :favorite-drink)))
human-c, the class of humans, with no
direct superclasses, and with two slots,
:favorite-drink. (In this document, for
pedagogic purposes only, we'll use the convention whereby
variables that bind to classes are suffixed
slot names are prefixed
:. You can name classes and
slots any way like.)
An instance of a class is defined with the
a class argument followed optionally by additional arguments
that are in twosomes, where the first element of a twosome
names a slot in the class, and the second element specifies
the value we want that slot to have in the current instance.
(define Telemakhos (make-instance human-c ':name "Telemakhos" ':favorite-drink (string-append "warm" " " "milk")))
Telemakhos is a particular human, an instance of
human-c. His name is
"Telemakhos", and his
favorite drink is
Both the class and the slots of an instance can be read. Only the slots can be mutated.
The slots of a class instance can be read using the
(slot-value Telemakhos ':name) "Telemakhos"
which is the
:name slot of Telemakhos.
(slot-value Telemakhos ':favorite-drink) "warm milk"
The slots of a class instance can be written using the
(set-slot-value Telemakhos ':favorite-drink "tequila")
:favorite-drink slot of Telemakhos to
"tequila". Henceforth, whenever you access this slot,
you will find
"warm milk", ie,
(slot-value Telemakhos ':favorite-drink) "tequila"
The class instance Telemakhos maintains its identity through
slot mutation. It's still the same object. Only its
:favorite-drink has changed. The
isn't sacred either -- it's settable too. You can say
(set-slot-value Telemakhos ':name "Telemachus")
and Telemakhos remains the same, albeit with a new name.
You can find the class of an instance using the predicate
returns the value of
human-c (something that is
human-c), because Telemakhos is an
instance of the class
Don't try to print the class value, unless your Scheme supports the finite printing of circular structures! Classes can be notoriously circular (sec 7.1).
You can define subclasses of classes. Eg,
(define schemer-c (make-class (human-c) (:favorite-dialect)))
defines a new
schemer-c class that is a subclass of the
human-c class is thus a
schemer-c have not only the slot
:favorite-dialect, but inherit all the slots of
human-c. Thus, they have the
:favorite-drink slots, even though the call to
make-class above doesn't mention these slots. Eg,
(define Odysseus (make-instance schemer-c ':name "Odysseus"))
we find that
(slot-value Odysseus ':name) "Odysseus"
The following slots exist, but haven't been initialized:
(slot-value Odysseus ':favorite-drink) :uninitialized (slot-value Odysseus ':favorite-dialect) :uninitialized
schemer-c have a slot called
(slot-value Odysseus ':favorite-dialect) =ERROR slot :favorite-dialect not found
(class-of Odysseus) returns the value of
Note, however, that Odysseus is an instance of both
class-of returns the
most specific class of which its argument is an
You can determine if a class is a subclass of another using
(subclass? schemer-c human-c) #t
thus confirming that Schemers are indeed human. On the other hand,
(subclass? human-c schemer-c) #f
for alas, not all humans are Schemers.
Subclasses can inherit from more than one class. Let's define the class of Common Lisp programmers:
(define lisper-c (make-class (human-c) (:favorite-loop-construct)))
We can now define a class
eclectic-lisper-c for humans that
program in both Scheme and Common Lisp:
(define eclectic-lisper-c (make-class (schemer-c lisper-c) (:favorite-other-language)))
eclectic-lisper-c is a subclass of both
lisper-c, and will inherit slots from both. Thus an
eclectic Lisper will have the following five slots:
:favorite-drink, inherited from
:favorite-dialect, inherited from
:favorite-loop-construct, inherited from
:favorite-other-language, directly from
We now define some sample classes and instances that we will use later on in this document:
Let's define some classes of food.
food-c is a class
that contains slots called
The value of the
:wholesomeness slot is typically a real
number between 0 and 1 stating how good the food is for you.
(define food-c (make-class () (:name :wholesomeness)))
Beverages and snacks are food:
(define beverage-c (make-class (food-c) ())) (define snack-c (make-class (food-c) ()))
Here are some instances of people, using the classes
(sec 4, sec 5):
(define Diomedes (make-instance schemer-c ':name "Diomedes")) (define Nestor (make-instance lisper-c ':name "Nestor")) (define Menelaos (make-instance lisper-c ':name "Menelaos")) (define Penelope (make-instance eclectic-lisper-c ':name "Penelope"))
We already have Telemakhos, an instance of
Odysseus, an instance of
Here are some foods, using
(define beer (make-instance beverage-c ':name "beer" ':wholesomeness .2)) (define coke (make-instance beverage-c ':name "coke" ':wholesomeness .4)) (define milk (make-instance beverage-c ':name "milk" ':wholesomeness 1)) (define candy (make-instance snack-c ':name "candy" ':wholesomeness .1)) (define french-fries (make-instance snack-c ':name "french fries" ':wholesomeness .4)) (define carrots (make-instance snack-c ':name "carrots" ':wholesomeness 1))
We will now explain the object classification. First, some
informal notation: We will use the term ScmObj class
for a class created using ScmObj, typically using
make-class. We will use ScmObj class instance for
class instances created using ScmObj, typically by calling
make-instance on a ScmObj class. We will use ScmObj
object to describe both ScmObj classes and ScmObj class
We will use pre-ScmObj object to describe the objects that you had in Scheme before loading ScmObj. We will use Scheme object to describe any object you can think of in Scheme, before or after loading ScmObj.
Thus: Scheme objects include both pre-ScmObj and ScmObj objects, and ScmObj objects include both ScmObj classes and instances of ScmObj classes.
We are now ready to describe where ScmObj classes stand in relation to ScmObj class instances, and where ScmObj objects stand in relation to pre-ScmObj objects.
From the discussion thus far, it would appear that there are two kinds of ScmObj objects:
a ScmObj class that specifies how its instances look like; and
a ScmObj class instance, a particular representative of a ScmObj class.
Actually, this distinction is blurrable. All ScmObj classes
are themselves instances of a distinguished ScmObj class
no exception -- it is an instance of itself.
In other words, all ScmObj objects -- both classes and
instances -- are instances of instances of [sic]
standard-class. To use CLOS terminology,
standard-class is a metaclass of (ie, ``class
of class of'') all ScmObj objects.
Thus, when you invoke
make-class, you are actually
make-class macro is still convenient,
because it does some extra bookkeeping that appropriately
fills all the slots of the created
The previous section described the ScmObj objects: some of them are ScmObj classes; all of them are ScmObj class instances.
But what about the objects of Scheme itself, the ones it had prior to loading ScmObj, viz, objects like booleans, numbers, characters, procedures, pairs, strings, and vectors? Are they instances of any class?
Yes they are. All the objects of Scheme, whether ScmObj or
pre-ScmObj, are considered to be instances of the
#t (the boolean), the only class
that is not a ScmObj object.
#t is implicitly a
superclass of every class, and as such is the least
#t too is an instance of
#t. Thus it
#t is a metaclass of all Scheme objects.
Note that pre-ScmObj objects can claim only
#t as their
class. ScmObj objects, on the other hand, can claim at least
#t class as their class (in addition to
We can exploit this to operationally distinguish between
ScmObj and pre-ScmObj objects -- simply call
the object. A pre-ScmObj object returns
#t, while a
ScmObj object returns some non-
#t class. (Recall that
class-of returns the most specific class: if the object
has any non-
#t class at all, then
#t, being less
specific, won't be the class that is returned.)
At the metaclass level, note that pre-ScmObj objects can only
#t as their metaclass, whereas ScmObj objects can
standard-class as their
ScmObj has no metaclass other than
standard-class. (In CLOS, other metaclasses
can be posited. The CLOS analog of
:metaclass argument, which specifies what
the metaclass of the class instances should be. Typically,
these metaclasses are defined as subclasses of
Generic procedures are procedures that can be specialized to the classes of their arguments. Each specialization of a generic procedure is called a method. When applying a generic procedure to a set of arguments, the most specific method vis-a-vis the arguments' classes is chosen.
Let's first declare a generic procedure using the
(define ingests (make-generic-procedure person food))
This defines a procedure called
requires two arguments. Presumably it describes the
fact of person ingesting food. (We are forced here
to use the Latinate ``ingests'' rather than the Saxon
``eats'', because we will subsequently distinguish between
eating (solids) and drinking (liquids).)
We now describe how to add methods to the generic procedure
ingests, and how to use it.
We are now ready to define methods describing the eating and
drinking habits of the people introduced in sec 6.2. To do this, we use the macro
add methods to the generic procedure
(sec 8). (The
statements made about Schemers' and Lispers' eating and
drinking propensities are not based on hard fact. They are
mere examples used for elucidation.)
Schemers will drink something only if it's at least .5
wholesome: (These examples require the
(defmethod ingests ((p schemer-c) (f beverage-c)) (if (>= (slot-value f ':wholesomeness) .5) (format #t "~a sips some ~a.~%" (slot-value p ':name) (slot-value f ':name))))
The first subexpression of
defmethod is the generic
procedure. In this case, it is
The second subexpression is a lambda-list of arguments to
the method. This lambda-list starts off with the
required arguments, where each argument is specified
as a two-element list: the parameter followed by its class.
The number of required arguments is the same as the number
of arguments specified by
make-generic-procedure for the
generic procedure. The rest of the lambda-list may contain
additional arguments, including a ``rest'' argument. In the
case above, the required arguments are
p of class
f of class
beverage-c. There are
no additional arguments.
After the second subexpression, we have the method body.
Proceeding with other methods for
ingests: no snack
is too lowly for a Schemer:
(defmethod ingests ((p schemer-c) (f snack-c)) (format #t "~a wolfs down some ~a.~%" (slot-value p ':name) (slot-value f ':name)))
Lispers are open to any drink:
(defmethod ingests ((p lisper-c) (f beverage-c)) (format #t "~a guzzles some ~a.~%" (slot-value p ':name) (slot-value f ':name)))
Lispers won't eat anything that isn't at least .5 wholesome:
(defmethod ingests ((p lisper-c) (f snack-c)) (if (>= (slot-value f ':wholesomeness) .5) (format #t "~a pecks at some ~a.~%" (slot-value p ':name) (slot-value f ':name))))
The default method: humans eat and drink anything:
(defmethod ingests ((p human-c) (f food-c)) (format #t "~a consumes ~a.~%" (slot-value p ':name) (slot-value f ':name)))
Note that ScmObj methods can specialize on more than one argument class. In other words, ScmObj methods, like CLOS methods, are multimethods.
We can now offer Telemakhos, Odysseus, Diomedes, Nestor,
Menelaos, and Penelope some beer, coke, milk, candy, french
fries, and carrots. We call the generic procedure
ingests, and let it dispatch the appropriate method
based on the class of the
person and the
(for-each (lambda (person) (for-each (lambda (food) (eat person food)) (list beer coke milk candy french-fries carrots))) (list Telemakhos Odysseus Diomedes Nestor Menelaos Penelope))
You will find that, as expected,
Telemakhos consumes everything in sight;
the Schemers Odysseus and Diomedes wolf down candy, french fries and carrots but sip only milk; and
the Lispers Nestor and Menelaos guzzle beer, coke and milk but eat only carrots.
But how does the eclectic Lisper Penelope, who is both
Schemer and Lisper, fare? We'll find that for the purposes
of nourishment, Penelope sits down with the Schemers. She
too wolfs down candy, french fries and carrots but sips only
milk, eschewing the more Lispy delights of beer and coke.
This is because the class
lisper-c in its superclass list.
The methods described thus far are primary methods.
You can add auxiliary methods that perform set-up
actions before or clean-up actions after the
primary method. Such methods are called
:after methods respectively.
Eg, let's say that Schemers wear a napkin before ingesting anything, while Lispers always put away the plate after ingesting anything. Thus,
(defmethod ingests :before ((p schemer-c) (f food-c)) (format #t "~a puts on a napkin.~%" (slot-value p ':name))) (defmethod ingests :after ((p lisper-c) (f food-c)) (format #t "~a puts away the plate.~%" (slot-value p ':name)))
Note that these methods use a qualifier --
:after -- as the second
defmethod. Primary methods could
:primary qualifier, but it's optional.
Now feed our people some more carrots and observe their table manners:
(for-each (lambda (person) (ingests person carrots)) (list Telemakhos Odysseus Diomedes Nestor Menelaos Penelope))
You'll find that
Telemakhos doesn't bother with any niceties;
the Schemers Odysseus and Diomedes put on a napkin before eating but don't care to put away the plate; and
the Lispers Nestor and Menelaos forget the napkin but remember to put away the plate.
Once again, how about eclectic Lisper Penelope? This time, we find that she takes the best from both her heritages. She puts on a napkin and puts away the plate.
:after methods are specified,
ScmObj executes all of them, the
:before methods in
most-specific-first order, and the
:after methods in
most-specific-last order. This is in keeping with CLOS's
standard method combination.
There is one other kind of auxiliary method, the
:around method. The most specific
method is executed in lieu of the primary method. If this
call-next-method, then the next most
:around method, or if there isn't any, the
most specific primary method is invoked. Primary methods
can also invoke
call-next-method -- this causes the
next most specific primary method to kick in.
next-method? can be used in both
:around and primary methods to ascertain that there
is a next method that can be called using
Eg, let's define a generic procedure for whether a person likes a food item:
(define likes (make-generic-procedure person food))
Based on our previous procedure for
easy to write the methods for
likes. Remember that
in general, people like all food, while Schemers like all
snacks, but only drinks that are at least .5 wholesome. The
primary methods for these ``facts'' are, in analogy with
(defmethod likes ((p human-c) (f food-c)) #t) (defmethod likes ((p schemer-c) (f snack-c)) #t) (defmethod likes ((p schemer-c) (f beverage-c)) (>= (slot-value f ':wholesomeness) .5))
Let's define subclasses of
for aged versions of these classes:
(define aged-mixin (make-class () ())) (define aged-beverage-c (make-class (aged-mixin beverage-c))) (define aged-snack-c (make-class (aged-mixin snack-c)))
And now for some instances of old drinks and eats:
(define lutefisk (make-instance aged-snack-c ':name "lutefisk" ':wholesomeness .3)) (define champagne (make-instance aged-beverage-c ':name "champagne" ':wholesomeness .6)) (define toddy (make-instance aged-beverage-c ':name "toddy" ':wholesomeness .2))
We can now specialize the
likes methods to
accommodate these new classes of food. Schemers don't like
aged snacks at all. In the case of aged drinks, they
overturn their usual judgment about drinks. Schemers have
no use for an old drink that offers them the same level of
wholesomeness that they like in fresh drinks. What they
crave is precisely the unwholesomeness of the old
drink. This state of affairs can be described with just two
(defmethod likes :around ((p schemer-c) (f aged-snack-c)) #f) (defmethod likes :around ((p schemer-c) (f aged-beverage-c)) (not (call-next-method)))
Note the use of the qualifier
Now let's try out an aged snack lutefisk, and two aged drinks, the wholesome champagne and the unwholesome toddy, on Schemer Odysseus.
(list (likes Odysseus lutefisk) (likes Odysseus champagne) (likes Odysseus toddy)) (#f #f #t)
indicating that Odysseus hates lutefisk and champagne, but loves toddy. De gustibus non est disputandum.
ScmObj is very similar to the default protocol in CLOS, the Common Lisp Object System. Thus, reading up on CLOS will help in learning and using ScmObj.
The major difference between ScmObj and CLOS is that ScmObj has no metaobject protocol. A minor difference is that the ScmObj primitives are more Schemely: they don't have the class v. name indirection present in CLOS, nor do they have slot or class options.
The following are some books that describe CLOS:
Guy L Steele, Jr, Common Lisp: The Language, 2nd ed,
Digital Press, 1990. Chap 28. A bit on the terse side.|
Sonya E Keene, Object-oriented Programming in Common Lisp:
A Programmer's Guide to CLOS, Addison-Wesley, 1989.
Tutorial-like, with examples.|
Andreas Paepcke, ed, Object-oriented Programming: The CLOS
Perspective, MIT Press, 1993. Chap 1, by Linda
G DeMichiel, contains a brief introduction.|
Patrick H Winston and Berthold K P Horn, Lisp, 3rd
ed, Addison-Wesley, 1993. Chaps 14, 21 and 22. Very
brief introduction, but enough for our purposes.|