Classes and objects in DrScheme

The DrScheme implementation of Scheme provides a large number of primitives beyond those that are described in the Revised5 report. In particular, it provides much more support for programming in the object-oriented style than standard Scheme.

DrScheme encourages programmers to think of objects as belonging to classes. Objects that belong to the same class have the same fields and accept the same messages. In DrScheme, a class is itself a value, over and above the individual objects that belong to that class. A class is generally introduced by means of a special class-expression, which lists the fields that chararacterize objects of the class and the messages that they respond to.

These fields and messages are collectively called the instance variables of the class. What we've been calling a ``message'' is represented in a DrScheme class as an instance variable that happens to have a procedure as its value. When an object receives a message, it responds by invoking that procedure.

Here, for instance, is how one would define a DrScheme class, switch%, and two objects of that class, similar to the lamp-switch and vacuum-cleaner-switch objects in the lab on object-oriented programming. We'll consider the syntactic details of class-expressions later on -- for now, just note how the state field and the :toggle! and :show-position messages are represented and how the constructor is invoked.

(define switch%
  (class object% ()
    (private
      (state #f))
    (public
      (:toggle! (lambda () (set! state (not state))))
      (:show-position (lambda () (if state 'on 'off))))
    (sequence
      (super-init))))

(define lamp-switch (make-object switch%))
(define vacuum-cleaner-switch (make-object switch%))

To send a message to an object, one uses a primitive procedure called send instead of invoking the object directly:

> (send lamp-switch :show-position)
off
> (send lamp-switch :toggle!)
> (send lamp-switch :show-position)
on

As another illustration of how classes are constructed, here is the tally example from that earlier lab, rewritten in the DrScheme style:

(define tally%
  (class object% ()
    (private
      (contents 0))
    (public
      (:show-contents (lambda () contents))
      (:set-contents-to-zero! (lambda () (set! contents 0)))
      (:bump! (lambda () (set! contents (+ contents 1)))))
    (sequence
      (super-init))))

Exercise 1

Create an object of the tally% class and send it some messages. Confirm that it works like the tallies in the lab on object-oriented programming.


A class-expression in DrScheme consists of

all enclosed in parentheses.

In DrScheme, a class is always derived from some other, more general class. To get the process started, there is a primitive class object% from which all others are derived, directly or indirectly. You'll notice that the superclass expression in the definition of switch% and tally% is `object%'. This indicates that each of those classes is derived directly from the object% class.

The significance of derivation is that if a class, say child%, is derived from a class parent%, then objects of the child% class inherit, implicitly, all of the instance variables of the parent% class. In particular, objects of the child% class automatically respond to any messages that objects of the parent% class respond to. The child% class can extend the parent% class, adding additional fields and responding to additional messages; it can also override instance variables of the parent% class, in effect replacing them with its own similarly named instance variables.

The object% class is basically null: It has no accessible fields (at least, none that are accessible to programmers or users) and responds to no messages. Classes derived directly from object% inherit only ``objectness.''

A instance-variable-clause consists of a pair of parentheses surrounding one of the following constructions:

At this point, we can make sense of definitions like this one, which is the translation of our queue constructor (from the lab on queues) into DrScheme's notation:

(define queue%
  (class object% ()
    (private
      (fore (list 'dummy-header))
      (aft fore)
      (size 0))
    (public
      (:empty? (lambda () (zero? size)))
      (:enqueue! (lambda (new)
                   (set-cdr! aft (list new))
                   (set! aft (cdr aft))
                   (set! size (+ size 1))))
      (:dequeue! (lambda ()
                   (if (null? (cdr fore))
                       (error "queue%:dequeue!: the queue is empty")
                       (let ((removed (cadr fore)))
                         (set-cdr! fore (cddr fore))
                         (if (null? (cdr fore))
                             (set! aft fore))
                         (set! size (- size 1))
                         removed))))
      (:front (lambda ()
                (if (null? (cdr fore))
                    (error "queue%:front: the queue is empty")
                    (cadr fore))))
      (:size (lambda () size)))
    (sequence
      (super-init))))

The three private instance variables correspond to the three static variables in our earlier definition, and the public instance variables, which all have procedures as their values, correspond to the messages. Note that the procedure that implements the :enqueue! message takes one argument -- the item to be added at the rear of the queue -- while the other messages take none.


Exercise 2

Create a queue, enqueue the symbols 'foo, 'bar, and 'baz, dequeue the front element, and check the size of the queue.


Exercise 3

Add a :print message to the queue class.


Exercise 4

Translate our implementation of stacks (from the lab on stacks) into a DrScheme class definition.


Initialization parameters

So far, none of the classes that we have defined has had any initialization parameters -- the list following the superclass expression has been empty. Initialization parameters are used when the objects of a class vary in the initial contents of their fields.

For instance, here's the definition of a box% class in which the constructor takes, as its argument, the initial contents of the box:

(define box%
  (class object% (initializer)
    (private
      (contents initializer))
    (public
      (:show-contents (lambda () contents))
      (:change-contents! (lambda (new)
                           (set! contents new))))
    (sequence
      (super-init))))

To create a box, we supply the initialization argument as an additional argument to DrScheme's make-object procedure:

> (define my-box (make-object box% 'jds))
> (send my-box :show-contents)
jds
> (send my-box :change-contents! 'hmw)
> (send my-box :show-contents)
hmw

If we want some boxes that are ``restorable,'' in the sense that they remember their original contents and can receive a :restore-initial-contents! message, we can extend the box% class, adding a new private field for the initial contents:

(define restorable-box%
  (class box% (initializer)
    (private
      (initial-contents initializer))
    (inherit :change-contents!)
    (public
      (:restore-initial-contents!
       (lambda ()
         (send this :change-contents! initial-contents))))
    (sequence
      (super-init initializer))))

The inherit clause makes the identifier :change-contents! available as the name of a box% message, so that we can use it in writing the procedure that implements our new message. In a DrScheme class definition, the identifier this always stands for the very object that is receiving the message, so `(send this ...)' means ``send a message to this very object.'' Since the restorable-box% class is derived from the box% class, restorable-box%-type objects can receive all the messages that box%-type objects can receive, such as the :change-contents! message, so it makes sense for a restorable-box% object to send this message to itself.

The call to super-init has a slightly different form in this case, because the initialization process for the parent class, box%, requires an argument, corresponding to its initialization parameter. (In all previous cases, the parent class was object%, which has no initialization parameters, so super-init took no arguments.)

Here's how a restorable box behaves:

> (define my-restorable-box (make-object restorable-box% 'jds))
> (send my-restorable-box :show-contents)
jds
> (send my-restorable-box :change-contents! 'hmw)
> (send my-restorable-box :show-contents)
hmw
> (send my-restorable-box :restore-initial-contents!)
> (send my-restorable-box :show-contents)
jds

Exercise 5

Define a traffic-light% class that is always in one of four states, represented by the symbols 'red, 'yellow, 'green, and 'flashing-red. A traffic light can respond to four messages:

Give the class a initialization parameter specifying its initial state.


Exercise 6

Derive from the tally% class defined above a clicker% class, containing objects that receive the same messages as tally% objects, and in addition a message called :advance!, which takes one argument, a natural number, and increases the internal counter by the specified amount:

> (define my-clicker (make-object clicker%))
> (send my-clicker :show-contents)
0
> (send my-clicker :bump!)
> (send my-clicker :bump!)
> (send my-clicker :show-contents)
2
> (send my-clicker :advance! 7)
> (send my-clicker :show-contents)
9
> (send my-clicker :set-contents-to-zero!)
> (send my-clicker :show-contents)
0
> (send my-clicker :advance! 5)
> (send my-clicker :bump!)
> (send my-clicker :advance! 78)
> (send my-clicker :show-contents)
84

Note: Since the contents instance variable of the tally% class is private, it is accessible only through messages in that class. So to implement :advance!, you'll have to arrange for the clicker send itself enough bump! messages to push the internal counter up to the correct new value.


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/classes-and-objects.xhtml

created May 2, 2000
last revised May 3, 2000

John David Stone (stone@cs.grinnell.edu)