We've shown you how to implement objects and classes with functions.
Why did we do this?
- To demonstrate that there is nothing magical about objects. They
reduce to concepts that we've already seen.
- To help you understand what objects are (most of us understand
things best once we implement them)
- To show you a concrete example of how programming languages evolve.
Object-oriented languages came about in part because programmers
were implementing objects manually with functions, in similar
fashion to what we've done here. Languages evolved to provide a
construct for objects (that does the cond -- aka dispatching -- for
you automatically).
Note that I'm not suggesting that you write OO code with
manually-implemented objects in practice. But it's useful to
understand objects, why they arise, and how else to think about
implementing them if you are in a language that doesn't have them.
-----------------------------------------------------------------------
Back to the topic of extending classes and objects with new code
though. Consider the following class for making circles:
;; make-circle : number -> (circle-interface -> value)
;; consumes radius and returns a circle object
(define make-circle-obj
(lambda (initrad)
(local [(define radius initrad)
(define (area) (* radius radius pi))
(define (resize newrad) (set! radius newrad))]
(lambda (service)
(cond [(symbol=? service 'area) area]
[(symbol=? service 'resize) resize])))))
[Note: to reduce confusion between objects and structures, we're going
to use a naming convention in which classes have the form "make-*-obj".]
Assume you want to enhance your circle object with more services.
Specifically, you want to add a perimeter service that computes the
perimeter of the circle. However, you want to do this under two
restrictions:
- we want to reuse to original code for make-circle-obj.
- we don't want to edit the original code for make-circle-obj. This
isn't an arbitrary restriction. If we can augment the functionality
of existing code without modifying it, then we don't risk breaking
other (existing) code that uses the original code.
Let's start by writing an object with the perimeter service, then
think about how to have it reuse the services from make-circle-obj.
(define make-circle-perim-obj
(lambda (...)
(local [(define (perimeter) (* 2 pi radius))]
(lambda (service)
(cond [(symbol=? service 'perimeter) perimeter])))))
Clearly, there is a problem here because perimeter needs radius and
radius is in circle-objects. So, we need (a) a circle object, and (b)
a way to get the radius out of a circle object. Let's augment
circle-objects to address the latter problem.
(define make-circle-obj
(lambda (initrad)
(local [(define radius initrad)
(define (area) (* radius radius pi))
(define (resize newrad) (set! radius newrad))]
(lambda (service)
(cond [(symbol=? service 'area) area]
[(symbol=? service 'resize) resize]
[(symbol=? service 'get-radius) radius])))))
Now, we need a circle objected that the perimeter object can use for
shared services. Let's add one to the local. The name "superobj"
arises from object-oriented programming practice, where the object
whose services another object reuses is called "super".
(define make-circle-perim-obj
(lambda (...)
(local [(define superobj (make-circle-obj ...))
(define (perimeter) (* 2 pi radius))]
(lambda (service)
(cond [(symbol=? service 'perimeter) perimeter])))))
Now, we can correct the use of radius in the perimeter function:
(define make-circle-perim-obj
(lambda (...)
(local [(define superobj (make-circle-obj ...))
(define (perimeter) (* 2 pi (superobj 'get-radius)))]
(lambda (service)
(cond [(symbol=? service 'perimeter) perimeter])))))
Notice, though, that the make-circle-obj needs an initial radius. So,
make-circle-perim-obj also needs initrad as an input.
(define make-circle-perim-obj
(lambda (initrad)
(local [(define superobj (make-circle-obj initrad))
(define (perimeter) (* 2 pi (superobj 'get-radius)))]
(lambda (service)
(cond [(symbol=? service 'perimeter) perimeter])))))
We can use this new class definition as follows:
> (define cp1 (make-circle-perim-obj 2))
> ((cp1 'perimeter))
#i12.566370614359172
What if we wanted to get the area of cp1? Right now, we'd get a "no
matching cond clause" error. We could add new cases to the service
cond for each service we want to reuse from make-circle-obj, but a
better solution is to simply pass any remaining service requests
directly to the superobj, without looking at what they are:
(define make-circle-perim-obj
(lambda (initrad)
(local [(define superobj (make-circle-obj initrad))
(define (perimeter) (* 2 pi (superobj 'get-radius)))]
(lambda (service)
(cond [(symbol=? service 'perimeter) perimeter]
[else (superobj service)])))))
Now, a circle-perim-obj has all the functionality of a circle-obj,
plus the perimeter service.
This situation, in which one object reuses all the services of
another, is called "inheritance". The circle-perim class inherits
services from the circle class.
Here's one more inheritence example. What if we wanted a new circle
class that had a center coordinate and a move service, as well as the
area and resize services (but not perimeter). That would look like:
(define make-circle-center-obj
(lambda (initrad initx inity)
(local [(define superobj (make-circle-obj initrad))
(define center (make-posn initx inity))]
(lambda (service)
(cond [(symbol=? service 'get-x) (posn-x center)]
[(symbol=? service 'get-y) (posn-y center)]
[(symbol=? service 'move) (lambda (newx newy)
(set! center (make-posn newx newy)))]
[else (superobj service)])))))
;> (define c6 (make-circle-center-obj 2 10 20))
;> (c6 'get-x)
;10
;> (c6 'get-y)
;20
;> ((c6 'move) 25 35)
;> (c6 'get-x)
;25
;> (c6 'get-y)
;35
Note the difference between this new class and circle-perim is that
circle-center needs three initialization parameters, but only the
initrad passes along to make-circle-obj.
We could draw the relationships between our three circle classes as
follows:
make-circle-obj
^ ^
| |
| |
make-circle-perim-obj make-circle-center-obj
where the arrows indicate that the class at the start of the arrow
shares the services of the class at the end of the arrow. This is the
standard picture of inheritence between classes. Note that
make-circle-obj does not have the perimeter or move services.