Our address book program from the last class consisted of a variable address-book and two functions lookup-number and add-to-address-book. Assume you've been running my address book program for several weeks and have built up a large list of numbers. Your roommate asks to use your computer, and starts writing a program as follows: (define address-book empty) ... What's now happened to your address book? It's gone. Destroyed. Since set! changes variable values, you've lost your entire address book simply because someone else wrote a program that uses the same variable name as your program. In this case, the reuse was accidental. You could also imagine a malicious roommate who destroys your address book on purpose (or someone changing the amount in an account in a banking program). Since set! is so destructive, we want to design our programs to prevent such mistakes or abuses from happening. To do this, we should follow a simple rule: Only set! variables declared in a local. This point arose briefly in our memoization lecture: we want to hide variables inside the functions that use them. In that lecture, we mentioned it as an issue of good style. In reality, it's an issue of security as well as good style. Today, we're going to discuss issues that arise when we follow this rule. Let's start with our traffic light program from the last class: ;; curr-color : one of 'red, 'green, or 'yellow ;; stores the current color of the traffic light (define curr-color 'red) ;; next-light : -> 'red, 'green, or 'yellow ;; effect : changes curr-color to reflect the next color of the light (define next-light (lambda () (local [(define next-color (cond [(symbol=? curr-color 'red) 'green] [(symbol=? curr-color 'yellow) 'red] [(symbol=? curr-color 'green) 'yellow]))] (set! curr-color next-color) curr-color))) We want to bury curr-color inside of next-light. How do we do it? As in the memoization example, we introduce a local before the lambda: ;; 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]))] (set! curr-color next-color) curr-color)))) This works, as we said earlier, because the lambda expression traps the dummy name for curr-color. 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-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))] (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? There are two ways. We could use defines to give names to the functions that we plan to use: > (define lookup-address (address-interface 'lookup)) > (define add-address (address-interface 'add)) > (lookup-address 'Kathi) false > (add-address 'Kathi 3) true > (lookup-address 'Kathi) 3 The other way is to not define the names, but to query the address-interface for the functions when we want to use them: > ((address-interface 'lookup) 'Kathi) false > ((address-interface 'add) 'Kathi 3) true > ((address-interface 'lookup) 'Kathi) 3 The first looks better because we don't keep querying for the function over and over. However, there are cases where the second is more appropriate (we'll see one later in the lecture or in next class, time depending). 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-interface : symbol -> address-book-service ;; returns the program for the requested service (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)) (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])))) > ((address-interface 'lookup) 'Kathi) 3 > ((address-interface 'update) 'Kathi 5) > ((address-interface '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-interface]. 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-interface2, 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're really like a function that returns a new address book program each time that we call it. We could define such a program as follows: ;; 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 This completes our example of writing programs that provide multiple services with memory. We've looked at how to hide variables when multiple programs need to access them and how to use symbolic messages to tell a program which service we want to provide. As a side note about programming: we've just taught you how to design objects (as in object-oriented programming). An object is simply a closure with multiple entry points, each entry point defined by a unique message. This course has taught you 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 (don't worry if you don't know what that is -- this is for the people who do). You could add inheritence to this program without about 15 or so lines of code.