When defining a record type in the manner described in the lab on records, the programmer may decide not to
define a mutator procedure for some fields (the size field of
a record of type shirt, for instance, or the
title field of a catalog card). This decision expresses the
programmer's judgement that such fields, once initialized, should never be
changed.
However, if many programmers are working together on a large programming
project, there is no way for the author of a library that contains such a
record type to enforce her decision. Records are vectors, and the
predefined vector-set! procedure can always be used to change
the contents of any position in a vector. The procedures provided
by the author of the record package are suggestions, not requirements; a
collaborator can always go behind those procedures to operate directly on
the vector that implements the record.
One of the basic ideas of the programming paradigm called object-oriented programming is to intercept such low-level interventions and treat them as errors. An object is a data structure that permits access to and modification of its elements only through a fixed set of procedures -- the object's methods. To request the execution of one of these methods, one sends the object a message that names the desired method, providing any additional arguments that the object will need as part of the message. Attempting to send an object a message that does not name one of its methods simply causes an error.
In Scheme, objects are implemented as procedures that take messages as
parameters and inspect them before acting on them. To provide the storage
locations that are being protected by this inspection mechanism, the
lambda-expression that denotes such a procedure is embedded
inside a let-expression that names and initializes the
protected locations.
Here's a simple example -- an object named sample-box that
contains only one field,contents, and responds to only one
message, show-contents:
(define sample-box
(let ((contents 42))
(lambda (message)
(if (eq? message 'show-contents)
contents
(error 'sample-box "unrecognized message")))))
> (sample-box 'show-contents)
42
> (sample-box 'set-contents-to-zero!)
Error in sample-box: unrecognized message.
Type (debug) to enter the debugger.
> (set! contents 0)
> (sample-box 'show-contents)
42
Both attempts to modify the contents field of sample-box
fail. Sending it the message set-contents-to-zero! doesn't
work, because the procedure is not set up to receive such a message. And
you can't reach the actual contents variable from outside the
sample-box procedure because that identifier is bound to the
storage location that contains 42 only inside the body of the
let-expression.
One could revise the procedure so that it would accept the message
set-contents-to-zero!:
(define zeroable-box
(let ((contents 57))
(lambda (message)
(cond ((eq? message 'show-contents) contents)
((eq? message 'set-contents-to-zero!) (set! contents 0))
(else (error 'zeroable-box "unrecognized message"))))))
> (zeroable-box 'show-contents)
57
> (zeroable-box 'set-contents-to-zero!)
> (zeroable-box 'show-contents)
0
Of course, there is no way for anyone to set the contents of this particular object to anything except zero, so now that the box has been zeroed its contents will remain zero forever.
Define a one-field object tally that responds to exactly three
messages: show-contents and
set-contents-to-zero!, as in zeroable-box, and
up!, which has the effect of increasing the number stored in
the contents field by 1. The initial value of that field
should be 0.
> (tally 'show-contents) 0 > (tally 'up!) > (tally 'show-contents) 1 > (tally 'up!) > (tally 'up!) > (tally 'up!) > (tally 'show-contents) 4 > (tally 'set-contents-to-zero!) > (tally 'show-contents) 0 > (tally 'up!) > (tally 'up!) > (tally 'show-contents) 2
In the preceding examples, we have created only one object of each type,
but it is not difficult to write a constructor procedure that can be called
repeatedly, to build and return any number of objects of a given type.
Suppose, for example, that we want to build several switches, each
of which is an object with one field, containing either the symbol
off or the symbol on and responding to only two
messages: show-position, which returns the current contents of
the field, and toggle!, which changes the field from
off to on or from on to
off. Here's a constructor for switches:
(define make-switch
(lambda ()
(let ((position 'off)) ; All switches are OFF when manufactured.
(lambda (message)
(cond ((eq? message 'show-position) position)
((eq? message 'toggle!) (if (eq? position 'off)
(set! position 'on)
(set! position 'off)))
(else (error 'switch "unrecognized message")))))))
Now let's manufacture a couple of switches and show that they can be toggled independently:
> (define lamp-switch (make-switch)) > (define vacuum-cleaner-switch (make-switch)) > (lamp-switch 'show-position) off > (vacuum-cleaner-switch 'show-position) off > (lamp-switch 'toggle!) > (lamp-switch 'show-position) on > (vacuum-cleaner-switch 'show-position) off > (lamp-switch 'toggle!) > (vacuum-cleaner-switch 'toggle!) > (lamp-switch 'show-position) off > (vacuum-cleaner-switch 'show-position) on
Because the make-switch procedure enters the
let-expression to create a new binding each time it is
invoked, each switch that is returned by make-switch gets a
separate storage location to put its position in. This storage location
retains its contents unchanged even between calls to the object and
independently of calls to any other object of the same type.
Define a make-tally procedure that constructs and returns
objects similar to the tally object you defined in the
previous exercise.
Create two tally objects and demonstrate that they can be incremented and reset independently.
In all of the preceding examples, the messages received by the object have
not included any additional arguments. Suppose that we want to define an
object similar to sample-box except that one can replace the
value in the contents field with any integer that is larger
than the one that it currently contains, by giving it the message
replace-with and including the new, larger value. We can
accommodate such messages by making the object a procedure of variable
arity, requiring at least one argument (the name of the method to be
applied) but allowing for more:
(define growing-box
(let ((contents 0))
(lambda (message . arguments)
(cond ((eq? message 'show-contents) contents)
((eq? message 'replace-with)
(if (zero? (length arguments))
(error 'growing-box
(string-append "replace-with method: "
"An argument is required"))
(let ((new-contents (car arguments)))
(cond ((not (integer? new-contents))
(error 'growing-box
(string-append
"replace-with method: "
"The argument must be an integer")))
((<= new-contents contents)
(error 'growing-box
(string-append
"replace-with method: "
"The argument must exceed the current contents")))
(else (set! contents new-contents))))))
(else (error 'growing-box "unrecognized message"))))))
> (growing-box 'show-contents)
0
> (growing-box 'replace-with 5)
> (growing-box 'show-contents)
5
> (growing-box 'replace-with 3)
Error in growing-box: replace-with method: The argument must exceed the current contents.
Type (debug) to enter the debugger.
> (growing-box 'show-contents)
5
> (growing-box 'replace-with 'foo)
Error in growing-box: replace-with method: The argument must be an integer.
Type (debug) to enter the debugger.
> (growing-box 'replace-with)
Error in growing-box: replace-with method: An argument is required.
Type (debug) to enter the debugger.
> (growing-box 'show-contents)
5
> (growing-box 'replace-with 7)
> (growing-box 'show-contents)
7
To define an object with several fields, some mutable, some not, one places
several bindings in the let-expression that encloses the
object procedure and provides mutation messages for the mutable fields but
not for the immutable ones. For instance, here is a constructor for a kind
of box that remembers its initial contents in a separate, immutable field
and can restore them on demand. Each such box responds to three messages:
show-contents, change-contents!, and
restore-initial-contents!:
(define make-restorable-box
(lambda (initializer)
(let ((contents initializer)
(initial-contents initializer))
(lambda (message . arguments)
(cond ((eq? message 'show-contents) contents)
((eq? message 'change-contents!)
(if (null? arguments)
(error 'restorable-box
(string-append "change-contents! method: "
"An argument is required"))
(set! contents (car arguments))))
((eq? message 'restore-initial-contents!)
(set! contents initial-contents))
(else
(error 'restorable-box "unrecognized message")))))))
> (define my-box (make-restorable-box 'JDS))
> (my-box 'show-contents)
jds
> (my-box 'change-contents! 'BG)
> (my-box 'show-contents)
bg
> (my-box 'restore-initial-contents!)
> (my-box 'show-contents)
jds
Define a constructor procedure, make-monitored-tally, for
objects similar to the tally objects from exercise 2 above,
except that each such object keeps track (in a separate field) of the total
number of messages that it has received. (The initial value for the new
field should be 0, and you should add 1 to this field as a side effect
every time the object is invoked.) A monitored-tally
object should also respond to a fourth message, report, by
returning the count of messages received.
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/spring-1998/object-oriented-programming.html
created December 4, 1997
last revised June 21, 1998