Classes and objects in DrScheme

Course links

Some of the extensions of Scheme that DrScheme provides (through its Languages menu) support many more primitives than are described in the Revised5 report on the algorithmic language Scheme. In particular, the MrEd extension, which is designed for the use of programmers who develop graphical user interfaces, provides much more support for programming in the object-oriented style than standard Scheme. (To execute the examples in this reading, therefore, you'll need to start DrScheme and use the Choose Language option of the Languages menu to switch to the Graphical (MrEd) extension.)

DrScheme encourages programmers to think of objects as belonging to classes. Objects that belong to the same class have the same fields (that is, static variables) 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 characterize 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 reading 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.

;;; switch%: a class of objects, each of which remembers whether it is
;;; on or off and can reveal or change its state on command

(define switch%
  (class object%

    ;; STATE is the state of the switch.  It is initially #F (that is,
    ;; off).

    (field (state #f))

    ;; When a SWITCH% receives the :SHOW-POSITION message, it returns the
    ;; symbol OFF if it is off and the symbol ON if it is on.

    (define/public :show-position
      (lambda ()
        (if state 'on 'off)))

    ;; When a SWITCH% has received the :TOGGLE message, it is on if it was
    ;; formerly off and off if it was formerly on.

    (define/public :toggle!
      (lambda ()
        (set! state (not state))))

    (super-new)))

(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 object from exercise 1 of the lab on object-oriented programming, rewritten in the DrScheme style:

;;; tally%: a class of objects, each of which remembers a natural number
;;; that can be incremented or reset to zero

(define tally%
  (class object%

    ;; Every TALLY% is initially 0.

    (field (contents 0))

    ;; When a TALLY% receives the :SHOW-CONTENTS message, it returns its
    ;; contents.

    (define/public :show-contents
      (lambda ()
        contents))

    ;; When a TALLY% has received the :SET-CONTENTS-TO-ZERO! message, its
    ;; contents are zero.

    (define/public :set-contents-to-zero!
      (lambda ()
        (set! contents 0)))

    ;; When a TALLY% has received the :BUMP! message, its contents are one
    ;; greater than they formerly were.

    (define/public :bump!
      (lambda ()
        (set! contents (+ contents 1))))

    (super-new)))

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 class-clause can take a variety of forms, fully documented in section 4.3 of the PLT MzLib libraries manual. The ones that we will use are of the following forms:

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

;;; queue%: a class of objects, each of which can contain any number of
;;; objects, accessible only in the order in which they were added

(define queue%
  (class object%

    ;; The queue is initially empty, but the underlying list always has a
    ;; dummy element at the front.

    (field (fore (list 'dummy-header))
           (aft fore)
           (size 0))

    ;; When a queue receives the :EMPTY? message, it reports whether it is
    ;; empty.

    (define/public :empty?
      (lambda ()
        (zero? size)))

    ;; When a queue receives the :ENQUEUE! message, it places the given
    ;; value at the rear.  All other values in the queue are in front of
    ;; it, in order by time of enqueuing.

    (define/public :enqueue!
      (lambda (new)
        (set-cdr! aft (list new))
        (set! aft (cdr aft))
        (set! size (+ size 1))))

    ;; When a queue receives the :DEQUEUE! message, it returns the
    ;; longest-enqueued value, the one at the front, and retains all other
    ;; values, in order by time of enqueuing.

    (define/public :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))))

    ;; When a queue receives the :FRONT message, it returns the value at
    ;; the front (available for dequeuing).

    (define/public :front
      (lambda ()
        (if (null? (cdr fore))
            (error "queue%:front: the queue is empty")
            (cadr fore))))

    ;; When a queue receives the :SIZE message, it returns the number of
    ;; values in the queue.

    (define/public :size
      (lambda ()
        size))

    ;; Invoke the parent constructor.

    (super-new)))

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.

Initialization parameters

So far, none of the classes that we have defined has had any initialization parameters. 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:

;;; box%: a class of objects, each of which remembers some value and can
;;; reveal or change that value on command

(define box%
  (class object% (init starter)

    (field (contents starter))

    ;; When a BOX% receives the :SHOW-CONTENTS message, it returns its
    ;; contents.

    (define/public :show-contents
      (lambda ()
        contents))

    ;; When a BOX% has received the :CHANGE-CONTENTS! message, it contains
    ;; the given value.

    (define/public :change-contents!
      (lambda (new)
        (set! contents new)))

    (super-new)))

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! 'hw)
> (send my-box :show-contents)
hw

Derivation and inheritance

Suppose that we want some objects that are similar to tallies, except that they respond to an additional message, :drop!, that (destructively) subtracts 1 from the contents field. Instead of copying the whole definition of tally% and adding a new define/public for the new method, we could derive our new class from the tally% class (assuming that DrScheme has already seen the definition of tally%):

;;; reducible-tally%: a class of objects, each of which remembers a natural
;;; number that can be incremented, decremented, or reset to zero

(define reducible-tally%
  (class tally%
    (inherit-field contents)
    (define/public :drop!
      (lambda ()
        (set! contents (- contents 1))))
    (super-new)))

The inherit-field clause makes the contents field accessible from the interior of the derived class; without that clause, the name contents would be undefined when we need it inside the definition of the drop! method.

Here is a reducible tally in action:

> (define tal (make-object reducible-tally%))
> (send tal :show-contents)
0
> (send tal :bump!)
> (send tal :drop!)
> (send tal :drop!)
> (send tal :drop!)
> (send tal :bump!)
> (send tal :drop!)
> (send tal :drop!)
> (send tal :show-contents)
-3

super-new

As another example of inheritance, let's write a restorable-box% class, deriving it from box% and adding a :restore-initial-contents! method to restore the contents field to the value it had when the box was created.

This time, we need to explicitly inherit the method 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 super-new-expression has a slightly different form in this case, because the initialization process for the parent class, box%, requires a value for its initialization parameter. The subexpressions of super-new are binding specifications for the initialization parameters of the parent class. (In all previous cases, the parent class was object%, which has no initialization parameters, so super-new had no subexpressions.)

;;; restorable-box%: a class of objects, each of which remembers some value
;;; and can reveal or change that value, or revert to its initial value, on
;;; command

(define restorable-box%
  (class box% (init rb-starter)
    (field
      (initial-contents rb-starter))

    (inherit :change-contents!)

    ;; When a RESTORABLE-BOX% has received the :RESTORE-INITIAL-CONTENTS!
    ;; message, it contains RB-STARTER.

    (define/public :restore-initial-contents!
      (lambda ()
        (send this :change-contents! initial-contents)))

    (super-new (starter rb-starter))))

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! 'hw)
> (send my-restorable-box :show-contents)
hw
> (send my-restorable-box :restore-initial-contents!)
> (send my-restorable-box :show-contents)
jds