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
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 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:
A Scheme expression, such as a procedure call or an if-expression.
When an object of this class is created, the expression class-clauses are
evaluated, in order, for their side effects. Normally, one of these
expressions is a super-new-expression, which carries out any
initialization that needs to be performed on instance variables inherited
from the parent class.
A list beginning with the keyword field and otherwise containing one
or more binding specifications (i.e., two-element lists in which the first
element is an identifier and the second an expression that supplies a value
for that identifier). The identifiers in the binding specification name
fields of each object of the class that is being defined.
A Scheme definition. The defined identifier stands for a new instance
variable that objects of this class will possess. The body of the
definition provides the initial value of the instance variable. The scope
of the definition is the class-expression itself, so that the
instance variable is ``private'' -- it is accessible only within the
class.
A Scheme definition, but with the keyword define replaced with define/public. Again the defined identifier stands for a new instance
variable, but this time the instance variable is a method, and the defined
identifier can be used in a send-expression to specify a message to
be sent to the object. Such instance variables are ``public,'' since they
can be used in send-expressions outside the class.
A list beginning with the keyword init and otherwise containing only
identifiers. The identifiers in this list are initialization
parameters of the class. As we'll see below, such parameters are bound to
values when an object of the class is constructed. They are private, and
can appear only in the definitions of fields in the class, not in the
definitions of methods. By default, a class has no initialization
parameters.
A list beginning with the keyword inherit-field and otherwise
containing only identifiers, which must be the names of fields of
the parent class. This clause makes the names of the inherited fields
visible inside this class, so that they can be used in subsequent
definitions and expressions.
A list beginning with the keyword inherit and otherwise containing
only identifiers, which must be the names of public methods of
the parent class. Again, this clause makes the names of the inherited
methods variables visible inside this class, so that they can be used in
subsequent definitions and expressions.
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.
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
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