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))))
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
class,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:
sequence, followed by some number of
expressions. When an object of this class is created, these expressions
are evaluated, in order, for their side effects. Normally, one of these
expressions is a call to the special procedure super-init,
which carries out any initialization that needs to be performed on instance
variables inherited from the parent class.public, followed by some number of identifiers
or binding specifications. Each identifier stands for a new instance
variable that objects of this class will possess. If the identifier stands
by itself after the keyword public, it initially has an
unspecified value; if it stands in a binding specification, its initial
value is the value of the expression that follows it.
private, followed by some number of
identifiers or binding specifications. The identifiers introduced in a
private clause name instance variables, just like those in
public clause, but the identifiers are bound only inside the
definition of this class.override, followed by some number of
identifiers of binding specifications. Again, the identifiers name
instance variables. In this case, however, they must also be the names of
public instance variables of the parent class, and the new bindings of the
identifiers replace the old ones in objects of this new class. Instance
variables introduced in override-clauses are public.inherit, followed by some number of
identifiers, which must be the names of public instance variables of the
parent class. This clause makes the names of the inherited instance
variables visible inside this class, so that they can be used in subsequent
binding expressions.rename, followed by some number of two-element
lists of identifiers. The second element in each two-element list must be
the name of some public instance variable of the parent class; the first
element, a new identifier, becomes an alias for that instance variable.
A clause of this kind is used when the programmer wants to override the
parent class's instance variable, typically a message, with a new message,
but the procedure that implements the new message needs to send the old
message; the alias provides a way to send the old message without getting
it mixed up with the new one.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.
Create a queue, enqueue the symbols 'foo, 'bar,
and 'baz, dequeue the front element, and check the size of the
queue.
Add a :print message to the queue class.
Translate our implementation of stacks (from the lab on stacks) into a DrScheme class definition.
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
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:
:show, which returns the current state.:emergency!, which sets the state to
'flashing-red, regardless of what it was before.:ok!, which takes one argument -- one of the symbols
'red, 'yellow, or 'green -- and sets
the state to that symbol.:cycle!, which has no effect if the current state is
'flashing-red, but changes a 'green state to
'yellow, a 'yellow state to 'red,
and a 'red state to 'green.Give the class a initialization parameter specifying its initial state.
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