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.