WARNING : You must move to the advanced or full language levels in DrScheme to use the constructs presented in this lecture. Consider an on-line address book. The address book provides two features: you can search it for a particular person's phone number, or you can add a new person and phone number to the address book. We'll use symbols for names and numbers for phone numbers (ignore dashes). Thus, we need to write two functions: ;; lookup-number : symbol -> (number or false) ;; returns number stored for person in phone book, or false ;; if no number for that person in the phone book (define (lookup-number name) ...) ;; add-to-address-book : symbol number -> true ;; updates address book with number for given name (define (add-to-address-book name phone) ...) These two programs need to exhibit the following behavior: after we add a name to the address book using add-to-address-book, lookup-number should always return a number. So, for example, the following sequence of calls should be correct, assuming 'Kathi was not in the original address book: > (lookup-number 'Kathi) false > (add-to-address-book 'Kathi 1234567) true > (lookup-number 'Kathi) 1234567 Note this is different from what we've done before. In our previous programs, every time you call a function on the same arguments, you get the same answer (this is characteristic of functions in mathematics, and a key idea underlying functional programming). Clearly, something new has to happen to allow us to write lookup properly. In particular, we need to introduce variables whose value is allowed to change. How would you write these programs? First, we need an address book: (define address-book empty) Whenever we define a variable whose value we expect to change, we should provide a statement (like a contract) indicating its type and what it contains. ;; An entry is a structure ;; (make-entry Na Nu) ;; where Na is a symbol and Nu is a number (define-struct entry (name number)) ;; address-book : list[entry] ;; keep track of the current address book entries (define address-book empty) We call variables such as address-book state variables, because they capture (and remember) the state of the program at some moment in time. Let's start by writing add-to-address-book: ;; add-to-address-book : symbol number -> true ;; updates address book with number for given name (define (add-to-address-book name num) (begin (set! address-book (cons (make-entry name num) address-book)) true)) Once we write programs that change the values of other variables, we should document what those changes will be. We therefore add an effect comment to any program that changes the value of a variable. ;; add-to-address-book : symbol number -> true ;; purpose : updates address book with number for given name ;; effect : changes the value of address-book to include entry for name (define (add-to-address-book name num) (begin (set! address-book (cons (make-entry name num) address-book)) true)) This code introduces a new keyword, set!. Set! is Scheme's assignment operator. You've used assignment before in other courses (as in x := x + 1), so the concept isn't new. Let's look briefly at how set! works in Scheme. -------------------------------------------------------------------------- A set! expression requires two pieces of information: a variable and an expression: (set! var some-exp) As with all expressions, DrScheme evaluates some-exp first, then changes the value of var to the value of some-exp. A set! expression has (returns) an invisible value. However, it has a visible effect because the old definition of var gets erased and replaced with the value of some-exp. We can see this by hand-evaluating the following expression: (define n 5) (begin (set! n (+ n 1)) n) Obviously, this should (and does) return 6. If we try to hand-evaluate it to see why, we get (define n 6) (begin n) which returns 6. So, set! effectively replaces an old define with a new one. Begin evaluates each expression in turn (until none are left), throwing away each expression after it evaluates it. Let's look at two more example programs: (define x 3) (define y 4) (begin (set! x y) (set! y x)) This becomes (define x 4) (define y 4) (begin (set! y x)) which in turn becomes (define x 4) (define y 4) Notice that, contrary to possible expectations, the original code does not swap the values of x and y. This is because expressions within a begin are evaluated in order. The effect of each set! within a begin occurs before the next one is evaluated. Let's try writing a swap program that works. (define u 4) (define v 5) (define (swap x y) (local [(define tmp x)] (set! x y) (set! y tmp))) (swap u v) What happens if I check the values of u and v after the call to swap? They still have their original values. This happens because set! on a parameter to a function does not affect anything outside of the function. As of now, you don't have the ability to write a swap function in Scheme. This non-functioning swap program introduces another useful construct though: local. Local provides a means for defining local variables. It is more powerful than the let that we have seen so far (for reasons that aren't important at the moment). Let's look at local more closely. Here's an example that uses two definitions: (define (expt5 x) (local [(define (square y) (* y y)) (define (cube z) (* z (square z)))] (* (square x) (cube x)))) What happens if we put this definition into the Definitions window and evaluate (expt5 2) in the interactions window? We get 32, as expected. What if we evaluate (cube 3) in the interactions window? We get an error. Why? The definitions inside of a local are only visible within the parentheses that enclose the local. Since the multiplication is inside the local, it can use the functions square and cube. Once we pass the parenthesis that closes (local, we can no longer use those definitions. We need to discuss how DrScheme evaluates a local, so that you know what programs using local actually do. Let's look at expt5 (the program given above), Evaluate (+ (expt5 2) 3). What does this become according to our evaluation rules? (+ (local [(define (square y) (* y y)) (define (cube z) (* z (square z)))] (* (square 2) (cube 2))) 3) Now, DrScheme evaluates (local [(define (square y) (* y y)) (define (cube z) (* z (square z)))] (* (square 2) (cube 2))) At this point, DrScheme copies your local definitions as if they were in the definitions window (you don't see them appear there, but it is as if a little person inside the machine moved them there). However, in order to keep someone from using them outside of their scope, DrScheme renames them (and changes the names in your program to reflect the new names). For example: (define (dummy-square y) (* y y)) (define (dummy-cube z) (* z (dummy-square z))) (* (dummy-square 2) (dummy-cube 2))) Note that you don't see any of these changes in the text of your program -- this all goes on behind the scenes when DrScheme evaluates your program. Now, DrScheme can finish evaluating your program normally, to yield the answer 35. To summarize the behavior of local: ...top-level definitions.. (local (defs) body) becomes: ...top-level definitions.. ...defs... (renamed for uniqueness) body (with renaming) -------------------------------------------------------------------------- Now, we can return to our address book program. Let's write lookup-number. ;; lookup-number : symbol -> (number or false) ;; returns number stored for person in phone book, or false ;; if no number for that person in the phone book (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))]))) As a reminder, here's the definition of add-to-address-book and address-book from before: ;; An entry is a structure ;; (make-entry Na Nu) ;; where Na is a symbol and Nu is a number (define-struct entry (name number)) ;; address-book : list[entry] ;; keep track of the current address book entries (define address-book empty) ;; add-to-address-book : symbol number -> true ;; purpose : updates address book with number for given name ;; effect : changes the value of address-book to include entry for name (define (add-to-address-book name num) (begin (set! address-book (cons (make-entry name num) address-book)) true)) This example motivates one rule for when to use set!. Use set! when a program must provide more than one service, and one of those services can change the underlying information. Let's consider another program. Traffic lights are controlled by programs. These programs must indicate the sequence of lights to display. We can write a simple controller for a traffic light as a program that consumes no inputs and produces the next color for the traffic light. We'd expect this program to operate as follows: > (next-light) 'green > (next-light) 'yellow > (next-light) 'red Notice that this function can return different answers, even when called the same way. Programs that need to be able to do this also require state. In these programs, the value that a function returns usually depends on some previous value that it computed. You can visualize this by drawing a picture of the sequencing of lights: red ----> green ----> yellow ^ | | | ----------------------- The picture shows the sequence, and how the next color to produce depends on the current color. We could write the next-light program as follows: ;; 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) (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 program uses something new: a lambda (function) with no arguments. Functions with no arguments are legal in Scheme, though they don't arise often. A function with no arguments is called a thunk. ---------------------------------------------------------------------- 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. Let's see what happens to our programs when we follow this rule. We'll start with the traffic light program. ;; 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) (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))) We want to bury curr-color inside of next-light. How do we do it? We know we need to put curr-color in a local, so why not try: ;; 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) (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))) If we run this program, we see the following behavior: > (next-light) 'green > (next-light) 'green > (next-light) 'green What happened? Remember how locals are evaluated. Each time you call next-light, scheme renames the local curr-color to a *new* dummy name. It's the new dummy name that gets updated on each set!, so there's no persistence of curr-color across multiple calls to next-light.