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.

--------------------------------------------------------------------

How hard is it to add a new service to the address book?  Not hard at
all.  We add the program for the new service to the local and add a
cond clause to return the new service.  As an example, let's add an
update service.  Update should consume a name and a number and replace
the person's entry in the phone book with the new entry.  Writing an
update function is easy (straightforward structural recursion).

;; address-ops : 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))
	  (define (update-address name num)
	    (set! address-book
	      (map (lambda (entry)
		     (cond [(symbol=? (entry-name entry) name)
			    (make-entry name num)]
			   [else entry]))
		   address-book)))]
    (lambda (service)
      (cond [(symbol=? service 'lookup) lookup-number]
	    [(symbol=? service 'add) add-to-address-book]
	    [(symbol=? service 'update) update-address]))))

> ((address-ops 'lookup) 'Kathi)
3
> ((address-ops 'update) 'Kathi 5)
> ((address-ops 'lookup) 'Kathi)
5

Now that your new address book program is finished, you and your
roommate decide that you each want your own address book.  You have
your program [address-ops].  How do you give your roommate an
address book that won't conflict with yours?  One option is to copy
the code and change the function name to address-ops2, but that
copies a lot of code.  Avoid copying code when possible.  If you add a
new feature to (or correct a mistake in) your address book program,
you don't want to have to make that same edit to every copy of the
program that you've handed out.

We approach this problem the same way we approach any situation with
duplicated code: create a helper function that has as parameters any
information that differs across the two uses of code.  In this case,
there are no differences, so the helper function takes no arguments:

;; make-address-book : -> (symbol -> address-book-service)
;; returns a new address book program
(define (make-address-book)
  (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))
	    (define (update-address name num)
	      (local [(define updated-book
			(map (lambda (entry)
			       (cond [(symbol=? (entry-name entry) name)
				      (make-entry name num)]
				     [else entry]))
			     address-book))]
		     (set! address-book updated-book)))]
    (lambda (service)
	(cond [(symbol=? service 'lookup) lookup-number]
	      [(symbol=? service 'add) add-to-address-book]
	      [(symbol=? service 'update) update-address]))))

> (define Kathi-book (make-address-book))
> (define John-book (make-address-book))
> ((Kathi-book 'add) 'Tom 4)
true
> ((Kathi-book 'lookup) 'Tom)
4
> ((John-book 'lookup) 'Tom)
false

What is make-address-book?  It's something that can be used to create
objects.  In other words, it's a class.

So, we've just 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.
More on this tomorrow.