ScmObj: An Object System for Scheme

 Dorai Sitaram 



1  Introduction

ScmObj is an object system for Scheme that provides

2  Classes

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.)

3  Instances

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.

3.1   Accessing Slots

The slots of a class instance can be read using the procedure slot-value. Eg,

(slot-value Telemakhos ':name)
[scmobj-Z-G-1.gif] "Telemakhos"

which is the :name slot of Telemakhos. Similarly,

(slot-value Telemakhos
  ':favorite-drink)
[scmobj-Z-G-2.gif] "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)
[scmobj-Z-G-3.gif] "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.

3.2   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).

4  Inheritance

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)
[scmobj-Z-G-4.gif] "Odysseus"

The following slots exist, but haven't been initialized:

(slot-value Odysseus ':favorite-drink)
[scmobj-Z-G-5.gif] :uninitialized
(slot-value Odysseus
  ':favorite-dialect)
[scmobj-Z-G-6.gif] :uninitialized

Neither human-c nor schemer-c have a slot called :favorite-island. So:

(slot-value Odysseus
  ':favorite-dialect)
=ERROR[scmobj-Z-G-7.gif]
slot :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)
[scmobj-Z-G-8.gif] #t

thus confirming that Schemers are indeed human. On the other hand,

(subclass? human-c schemer-c)
[scmobj-Z-G-9.gif] #f

for alas, not all humans are Schemers.

5  Multiple Inheritance

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.

6  Sample Classes and Instances

We now define some sample classes and instances that we will use later on in this document:

6.1   Sample Classes

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) ()))

6.2   Sample Instances

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))

7  ScmObj and Scheme

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.

7.1  Classes Are Class Instances Too

From the discussion thus far, it would appear that there are two kinds of ScmObj objects:

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.

7.2  Scheme Objects Are Class Instances Too

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.)

8  Generic Procedures and Methods

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.

8.1  Methods

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.

8.2  Calling Generic Procedures

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,

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.

9  :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

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.

10  :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))
[scmobj-Z-G-10.gif] (#f #f #t)

indicating that Odysseus hates lutefisk and champagne, but loves toddy. De gustibus non est disputandum.

11   References

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:

[1]   Guy L Steele, Jr, Common Lisp: The Language, 2nd ed, Digital Press, 1990. Chap 28. A bit on the terse side.

[2]   Sonya E Keene, Object-oriented Programming in Common Lisp: A Programmer's Guide to CLOS, Addison-Wesley, 1989. Tutorial-like, with examples.

[3]   Andreas Paepcke, ed, Object-oriented Programming: The CLOS Perspective, MIT Press, 1993. Chap 1, by Linda G DeMichiel, contains a brief introduction.

[4]   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.

Last modified: Thursday, August 21st, 2003 US/Eastern
HTML conversion by TeX2page 2003-08-16