[Current] [News] [Glance] [Discussions] [Instructions] [Search] [Links] [Handouts] [Outlines] [Readings] [Labs] [Homeworks] [Quizzes] [Exams] [Examples] [Fall2000.01] [Spring2000]
As you may recall, one of the key issues in the design of records is that the record designer have some control over the use of records. In particular, the designer might want to require that some fields be fixed and allow others to be mutable. The designer may also want to limit the legal values of some fields.
As we saw in the corresponding lab, it is relatively easy for someone other than the designer/implementer of the record to modify the record in ``inappropriate'' ways. For example, suppose that someone has written a student record type and someone else has written a related set of utilities. We might hope that the following would only behave correctly:
(load "student.ss")
(load "sams-student-stuff.ss") ; includes compute-gpa
(define report-gpa
(lambda (student)
(if (not (student? student))
(error "Bozo, that's not a student")
(display (compute-gpa student)))))
We have no guarantee as to whether or not the student record is still correct afterwards. (Other than crossing our fingers.)
At a more detailed level, there's nothing to stop someone from changing
a fixed field of a record by using vector-set!.
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. The custom is to precede the message names with colons.
The Scheme standard does not include objects. However, you can implement an object as a procedure that takes messages as parameters and inspects them before acting on them. Vectors provide the storage locations that are protected by the procedure.
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 (vector 42)))
(lambda (message)
(if (eq? message ':show-contents)
(vector-ref contents 0)
(error "sample-box: unrecognized message")))))
> (sample-box ':show-contents)
42
> (sample-box ':set-contents-to-zero! 671)
sample-box: unrecognized message
> (set! contents 0)
set!: cannot set undefined identifier: contents
> (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 (vector 57)))
(lambda (message)
(cond ((eq? message ':show-contents)
(vector-ref contents 0))
((eq? message ':set-contents-to-zero!)
(vector-set! contents 0 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.
In the preceding examples, we have created only one object of each type,
but it is not difficult to write a higher-order 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 (a Boolean
value) and responding to only two messages: ':show-position,
which returns 'on if the field contains #t and
'off if it contains #f, and
':toggle!, which changes the field from #t to
#f or from #f to #t. Here's a
constructor for switches:
(define make-switch
(lambda ()
(let ((state (vector #f))) ; All switches are off when manufactured.
(lambda (message)
(cond ((eq? message ':show-position)
(if (vector-ref state 0) 'on 'off))
((eq? message ':toggle!)
(vector-set! state 0 (not (vector-ref state 0))))
(else (error "switch: unrecognized message")))))))
Now let's manufacture a couple of switches and show that they can be toggled independently:
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 static variable to put its state in. This static variable
retains its contents unchanged even between calls to the object and
independently of calls to any other object of the same type.
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 (string-append "growing-box:replace-with: "
"an argument is required"))
(let ((new-contents (car arguments)))
(cond ((not (integer? new-contents))
(error (string-append
"growing-box:replace-with: "
"the argument must be an integer")))
((<= new-contents contents)
(error (string-append
"growing-box:replace-with: "
"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)
growing-box:replace-with: the argument must exceed the current contents
> (growing-box ':show-contents)
5
> (growing-box ':replace-with 'foo)
growing-box:replace-with: the argument must be an integer
> (growing-box ':replace-with)
growing-box:replace-with: an argument is required
> (growing-box ':show-contents)
5
> (growing-box ':replace-with 7)
> (growing-box ':show-contents)
7
[Current] [News] [Glance] [Discussions] [Instructions] [Search] [Links] [Handouts] [Outlines] [Readings] [Labs] [Homeworks] [Quizzes] [Exams] [Examples] [Fall2000.01] [Spring2000]
Disclaimer Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.
This page may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2000F/Readings/oop.html
Source text last modified Wed Dec 6 10:21:22 2000.
This page generated on Wed Dec 6 10:24:34 2000 by Siteweaver. Validate this page's HTML.
Contact our webmaster at rebelsky@grinnell.edu