ScmObj is an object system for Scheme that provides
classes with multiple inheritance;
generic procedures;
methods that can specialize on one or more arguments (``multimethods'');
:before, :after, and :around auxiliary
methods in addition to primary methods;
call-next-method and next-method? in
primary and :around methods; and
standard method combination a la the default protocol in CLOS.
ScmObj lets you define a new class with the make-class
macro. make-class takes two subexpressions: (1) a
list of direct superclasses and (2) a list of
slots. Eg,
(define human-c (make-class () (:name :favorite-drink)))
This defines human-c, the class of humans, with no
direct superclasses, and with two slots, :name and
:favorite-drink. (In this document, for
pedagogic purposes only, we'll use the convention whereby
variables that bind to classes are suffixed -c and
slot names are prefixed :. You can name classes and
slots any way like.)
An instance of a class is defined with the
make-instance procedure. make-instance takes
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.
Eg,
(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 "warm milk".
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
procedure slot-value. Eg,
(slot-value Telemakhos ':name)"Telemakhos"
which is the :name slot of Telemakhos.
Similarly,
(slot-value Telemakhos ':favorite-drink)"warm milk"
The slots of a class instance can be written using the
procedure set-slot-value. Eg,
(set-slot-value Telemakhos ':favorite-drink "tequila")
sets the :favorite-drink slot of Telemakhos to
"tequila". Henceforth, whenever you access this slot,
you will find "tequila", not "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 :name slot
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.
class-of
You can find the class of an instance using the predicate
class-of. Eg,
(class-of Telemakhos)
returns the value of human-c (something that is
eq? to human-c), because Telemakhos is an
instance of the class human-c.
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. human-c class is thus a
superclass of schemer-c.
Instances of schemer-c have not only the slot
:favorite-dialect, but inherit all the slots of
human-c. Thus, they have the :name and
:favorite-drink slots, even though the call to
make-class above doesn't mention these slots. Eg,
with
(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
Neither human-c nor schemer-c have a slot called
:favorite-island. So:
(slot-value Odysseus ':favorite-dialect) =ERRORslot :favorite-dialect not found
(class-of Odysseus) returns the value of schemer-c.
Note, however, that Odysseus is an instance of both
schemer-c and human-c. class-of returns the
most specific class of which its argument is an
instance.
You can determine if a class is a subclass of another using
the predicate subclass?. Eg,
(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 schemer-c
and lisper-c, and will inherit slots from both. Thus an
eclectic Lisper will have the following five slots:
:name and :favorite-drink, inherited from human-c;
:favorite-dialect, inherited from schemer-c;
:favorite-loop-construct, inherited from lisper-c; and
:favorite-other-language, directly from eclectic-lisper-c.
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 :name and :wholesomeness.
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
schemer-c, lisper-c and eclectic-lisper-c
(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 human-c, and
Odysseus, an instance of schemer-c.
Here are some foods, using beverage-c and snack-c
(sec 6.1):
(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
instances.
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
called standard-class. standard-class is
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
invoking make-instance on standard-class.
However, the make-class macro is still convenient,
because it does some extra bookkeeping that appropriately
fills all the slots of the created standard-class
instance.
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
distinguished class #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
specific class.
Clearly, #t too is an instance of #t. Thus it
follows that #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
one non-#t class as their class (in addition to #t).
We can exploit this to operationally distinguish between
ScmObj and pre-ScmObj objects -- simply call class-of on
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
claim #t as their metaclass, whereas ScmObj objects can
claim both #t and standard-class as their
metaclass.
ScmObj has no metaclass other than #t and
standard-class. (In CLOS, other metaclasses
can be posited. The CLOS analog of make-class takes
an optional :metaclass argument, which specifies what
the metaclass of the class instances should be. Typically,
these metaclasses are defined as subclasses of
standard-class.)
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
macro make-generic-procedure:
(define ingests (make-generic-procedure person food))
This defines a procedure called ingests that
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 defmethod to
add methods to the generic procedure ingests
(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 format
feature.)
(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 ingests.
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
schemer-c and 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 food
arguments:
(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 eclectic-lisper-c lists
schemer-c before lisper-c in its superclass list.
:before and :after Methods
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 :before and
: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 --
:before or :after -- as the second
subexpression of defmethod. Primary methods could
use the :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.
If several :before and :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.
:around Methods
There is one other kind of auxiliary method, the
:around method. The most specific :around
method is executed in lieu of the primary method. If this
method invokes call-next-method, then the next most
specific :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.
The predicate next-method? can be used in both
:around and primary methods to ascertain that there
is a next method that can be called using
call-next-method.
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 ingests, it's
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
those for ingests:
(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 beverage-c and snack-c
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
:around methods:
(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 :around.
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: