Consider the max-of-list function from the exam. One solution was ;; max-of-list : non-empty-list-of-numbers -> num ;; return the largest number in the input list (define (max-of-list anelon) (cond [(empty? (rest anelon)) (first anelon)] [(cons? (rest anelon)) (cond [(> (first anelon) (max-of-list (rest anelon))) (first anelon)] [else (max-of-list (rest anelon))])])) There's something unsatisfying about this program: in the cons? case, we compute (max-of-list (rest anelon)) twice. Writing the same expression twice is unsatifying for several reasons: - If we write an expression only once, then if we have to change it, we only need to change it in one place. This is called having a single point of control. - If the expression is long and tedious, we would rather not read (or write) it more than once. - It induces a lot of redundant computation. How much? Consider what happens if we have a list containing 6 numbers. Then we will compute max-of-list twice on a list containing 5 numbers. Each of these computations will compute max-of-list twice on a list containing 4 numbers, and so on. For a list containing n elements, we end up calling max-of-list 2^(n-1) times. For a large enough list (around 15 elements), you will notice DrScheme slowing down due to the extra computation. Therefore, we want to be able to call max-of-list only once. Today, we introduce another piece of Scheme syntax (your first in a month) to help with this situation: it's called local. Here's how max-of-list would appear using local. (To use this, you'll need to start using the Intermediate language level in DrScheme.) ;; max-of-list : non-empty-list-of-numbers -> num ;; return the largest number in the input list (define (max-of-list anelon) (cond [(empty? (rest anelon)) (first anelon)] [(cons? (rest anelon)) (local ((define maxrest (max-of-list (rest anelon)))) (cond [(> (first anelon) maxrest) (first anelon)] [else maxrest]))])) Why are there two parentheses between local and define? Local takes a list of definitions, each in proper Scheme definition format. The outer pair of parentheses enclose the list of definitions, which in the above case has only one element. 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. This raises an important issue in programming languages known as scoping. Scoping tells us where a definition is visible. You've been dealing with scoping all along, but perhaps without realizing it. Consider what happens if you type x at the DrScheme prompt. Unless you've done a (define x ...), DrScheme complains that x is an undefined identifier. However, if you write (define (f x) (+ x 3)), DrScheme doesn't complain because the x in (+ x 3) is within a function that has x as a parameter. Whenever you write a definition (define or define-struct), you are telling DrScheme where it can use your definition. Consider the following example (define (double x) (+ x x)) (define (some-math x) (local ((define (square y) (* y y)) (define (cube y) (* y (square y)))) (+ (square x) (cube x) (local ((define z (- (cube x) 3))) (square (double z)))))) What this program computes is irrelevant. What is relevant is where each variable and definition is visible. Double is visible everywhere, because it is not inside of a local. The programs square and cube are only visible inside of the first local. The identifier z is only visible inside the second local. The text contains several examples of drawing boxes around the parts of a program where a definition is visible. Look at those and make sure you see how to draw similar boxes on this example. Finally, we need to discuss how DrScheme evaluates a local, so that you know what programs using local actually do. Let's return to our earlier example expt5: (define (expt5 x) (local ((define (square y) (* y y)) (define (cube z) (* z (square z)))) (* (square x) (cube x)))) 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). 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 semantics of local: ...top-level definitions.. (local (defs) body) becomes: ...top-level definitions.. ...defs... (renamed for uniqueness) body (with renaming)