Yesterday, we left off with a broken next-light function. We are
trying to write next-light such that
- the curr-color variable is burying inside the next-light
definition
- curr-color is in the environment of the lambda for next-light, so
that all calls to next-light refer to the same value
Let's rewrite the current (broken) version of next-light so that the
lambda (and hence the closure location) is explicit:
;; next-light : -> 'red, 'green, or 'yellow
;; effect : changes curr-color to reflect the next color of the light
(define next-light
(lambda ()
;; curr-color : one of 'red, 'green, or 'yellow
;; stores the current color of the traffic light
(local [(define curr-color 'red)
(define next-color
(cond [(symbol=? curr-color 'red) 'green]
[(symbol=? curr-color 'yellow) 'red]
[(symbol=? curr-color 'green) 'yellow]))]
(begin
(set! curr-color next-color)
curr-color))))
Clearly, the curr-color definition needs to move outside of the lambda
so that curr-color is captured in the closure. This suggests two
places: before the "(define next-light" or after "(define next-light"
but before "(lambda ()". The former involves a global variable, which
we are trying to avoid. So, let's try the latter:
;; next-light : -> 'red, 'green, or 'yellow
;; effect : changes curr-color to reflect the next color of the light
(define next-light
;; curr-color : one of 'red, 'green, or 'yellow
;; stores the current color of the traffic light
(local [(define curr-color 'red)]
(lambda ()
(local [(define next-color
(cond [(symbol=? curr-color 'red) 'green]
[(symbol=? curr-color 'yellow) 'red]
[(symbol=? curr-color 'green) 'yellow]))]
(begin
(set! curr-color next-color)
curr-color)))))
This works because the closure for the lambda expression traps the
dummy name for curr-color in its environment. Each reference to
curr-color inside the lambda expression therefore refers to a
persistent variable.
Now, let's consider our address book program. Hiding the variable
here is a little harder than for traffic light because now there are
two functions that refer to address-book, not just one. What options
do we have?
1. Put a copy of address-book inside each function. That won't work
because then each function would be accessing its own private copy,
rather than sharing a single copy, as is needed.
2. Put the two functions that use address book as local functions
inside of one wrapper function.
Let's explore the second approach. What might such a function look
like?
(define (address-interface ...)
(local [(define address-book empty)
(define (lookup-number name)
(local [(define matches
(filter (lambda (an-entry)
(symbol=? name (entry-name an-entry)))
address-book))]
(cond [(empty? matches) false]
[else (entry-number (first matches))])))
(define (add-to-address-book name num)
(begin
(set! address-book
(cons (make-entry name num) address-book))
true))]
...))
The problem here is that we no longer have a way to use our original
programs, lookup-number and add-to-address-book. We need some way to
get those functions out of address-interface once they've captured the
dummy name for address-book. How could we do this?
Our first temptation might be to return a list containing the
functions, as follows:
(define (address-interface)
(local [(define address-book empty)
(define (lookup-number name)
(local [(define matches
(filter (lambda (an-entry)
(symbol=? name (entry-name an-entry)))
address-book))]
(cond [(empty? matches) false]
[else (entry-number (first matches))])))
(define (add-to-address-book name num)
(begin
(set! address-book
(cons (make-entry name num) address-book))
true))]
(list lookup-number add-to-address-book)))
This solution is bad for three reasons:
1. It doesn't scale. If I added 16 new features to my program, my
return list gets very complicated. How will I remember which function
is in which position in the list?
2. It's not flexible. I could write an address book program with tons
of features. A user who only wants some of those features should have
to know about all the unnecessary features.
3. The return type is arbitrary. We made a list because we didn't
know what else to do. However, a list of programs isn't a natural
return value for a program that should be returning one address book
program.
We really want to get back one program through which we can access all
of the services that we want to use. We need a way to tell the
program which service we want. Let's assign a symbolic name to each
service in our address book. We'll use 'lookup for lookup-number and
'add for add-to-address-book. Now, our address book program can
consume one of these names and give us back the program that we want:
;; An address-book-service is one of
;; - symbol -> number
;; - symbol number -> true
;; address-interface : symbol -> address-book-service
;; returns the program for the requested service
(define address-ops
(local [(define address-book empty)
(define (lookup-number name)
(local [(define matches
(filter (lambda (an-entry)
(symbol=? name (entry-name an-entry)))
address-book))]
(cond [(empty? matches) false]
[else (entry-number (first matches))])))
(define (add-to-address-book name num)
(begin
(set! address-book
(cons (make-entry name num) address-book))
true))]
(lambda (service)
(cond [(symbol=? service 'lookup) lookup-number]
[(symbol=? service 'add) add-to-address-book]))))
In this situation, we should not use else for the 'add service,
because there is a good chance that we'll want to add other services
later on.
How do we use our new program?
> ((address-ops 'lookup) 'Kathi)
false
> ((address-ops 'add) 'Kathi 3)
true
> ((address-ops 'lookup) 'Kathi)
3
This address-ops program resembles something you've seen before. It's
an object, in the usual sense of object-oriented programming. An
object is just a program construct that performs services based on
messages. More technically, an object is simply a closure with
multiple entry points, each entry point defined by a unique message.
So, in this course we have seen two of the three fundamental concepts
behind objects: data-driven processing (ie, templates) and
encapsulated services (today's example). To get object-oriented
programming, all you need is inheritence -- tune in tomorrow for that.