CS 227 Lecture -*- Outline -*- bring: sheet with fill-in-the-blank terms (let-letrec-defs.txt) focus: what let/letrec are good for, and then how they work. Note, this stuff is the foundation for chapter 7. for next time: there is too much here. Extra stuff has to be cut to bring this more into focus. Could be more inductive, with definition of let in terms of lambda coming at the end. * Locally Defined Values (Chapter 5, 5.2) We now begin to consider the problems of putting procedures together to form programs. "Programming in the Large" For example, baseball or recipe database Also starting to look more closely at what lambda does... ** problems *** Name space problems Each defined name we add to our program may be overridden by another definition of the same name. e.g., for helping procedures in iteration ------------ CONTROLLING THE NAME SPACE (DEFINE find (LAMBDA (recipes query) ; execute a find query . . . )) . . . (DEFINE find (LAMBDA (x lst) ; return the index of x in lst . . . )) ------------ If we could hide the specialized find in the query processor, we would avoid some, but not all problems like this. *** Abbreviation No good way to save the result of a subcomputation instead of computing it twice. ------------ (double-reverse-sublist '( (a b) (1 2) )) ==> ( (b a) (b a) (2 1) (2 1) ) (DEFINE double-reverse-sublist ; TYPE: (-> ((list T)) ; (list (list T))) (LAMBDA (ls) (IF (null? ls) '() (cons (reverse (car ls)) (cons (reverse (car ls)) (double-reverse-sublist (cdr ls))))))) ------------ Besides the inefficiency of computing twice, if the expression is complicated, we may not want to write it twice. ~~~~~ Q: Other examples? repeated use of (car ls) in deep recursion What can we do? **** Using a helping procedure ------------ (DEFINE double-reverse-sublist (LAMBDA (ls) (IF (null? ls) '() (double-helper (reverse (car ls)) (double-reverse-sublist (cdr ls)))))) (DEFINE double-helper (LAMBDA (to-double tail) (cons to-double (cons to-double tail)))) ------------ This avoids the computation of reverse twice, since it is computed once and passed to double-helper. But adds to the global environment. We could avoid that by putting the "value" of double-helper in the one spot it is used. (Don't want a name for something only used once, clutter of the global environment is not worth it in a big program.) ------------ (DEFINE double-reverse-sublist (LAMBDA (ls) (IF (null? ls) '() ((LAMBDA (to-double tail) (cons to-double (cons to-double tail))) (reverse (car ls)) (double-reverse-sublist (cdr ls)) ) ))) ------------ But yick! This is even harder to understand than before. So Scheme has a special keyword, let, that does what we are trying to do here. ------------ (DEFINE double-reverse-sublist (LAMBDA (ls) (IF (null? ls) '() (LET ((to-double (reverse (car ls))) (tail (double-reverse-sublist (cdr ls))) ) (cons to-double (cons to-double tail)) ) ))) ------------ Point out syntax of let Common problem of leaving out the extra parenthesis Another advantage of this syntax is that we do not need to pass everything to the (anonymous) helping function. ------------ (DEFINE double-reverse-sublist (LAMBDA (ls) (IF (null? ls) '() (LET ((to-double (reverse (car ls))) ) (cons to-double (cons to-double (double-reverse-sublist (cdr ls)))) ) ))) ------------ We'll get to useful examples in the next section. For now we'll concentrate on LET itself and a variant LETREC... ** let *** syntax ------------------ SYNTAX OF LET (LET ((var val) ...) body) ---------------- Do the following on-line ------------------ EXAMPLES (let ((x 3) (y 4)) (+ x y)) (LET ([x 3] [y 4]) ; ok in Chez Scheme (+ x y)) (define x 4) (let ((add2 (lambda (x) (+ x 2))) (b (* 3 (/ 2 12))) (newx (add1 x))) (/ b (+ newx (add2 b)))) (let ((w (cons 'wow '())) (al (cons 'a (cons 'list '())))) (cons w (cons 'what (cons al '())))) ------------------ Q: what would these last 2 would look like without let? *** meaning Show on the computer: (define add2 (lambda (x) (+ x 2))) (add2 3) ((lambda (x) (+ x 2)) 3) (let ((x 3)) (+ x 2)) ------------------ MEANING (LET ((var1 val1) ... (varn valn)) body) = ((LAMBDA (var1 ... varn) body) val1 ... valn) ------------------ **** translation of let into LAMBDA (and back) point out the body of the let becomes the body of the LAMBDA note the double parenthesis at beginning, operator, operand Work examples on the board... ------------------ EXAMPLES (LET ((x 3) (y 4)) (+ x y)) = ((LAMBDA (x y) (+ x y)) 3 4) = (+ 3 4) = 7 ------------------ Works inside other expressions too ------------------ (+ 2 (let ((a 3)) (+ a a))) = (+ 2 ((LAMBDA (a) (+ a a)) 3)) ------------------ This is useful in translating nested lets into LAMBDA, and thus in understanding nested procedures ------------------ (LET ((x 3) (y 4)) (LET ((x y) (y x)) (list x y) ) ) = (LET ((x 3) (y 4)) ((LAMBDA (x y) (list x y)) x y) ) = ((LAMBDA (x y) ((LAMBDA (x y) (list x y) ) y x) ) 3 4) ------------------ Have them do this one... (let ((b 6.2)) (let ((a 3)) (+ a b))) = (let ((b 6.2)) ((LAMBDA (a) (+ a b)) 3)) = ((LAMBDA (b) ((LAMBDA (a) (+ a b)) 3)) 6.2) *** bindings simultaneous ------------------- SOME IMPLICATIONS 1. LET MAKES SIMULTANEOUS BINDINGS a. (let ((y 1)) (let ((y (+ y 3)) (x (+ y 2))) (+ x y))) = 7 (NOT: 10, or 4) --------------------- do the calculations (using translation to lambda) How could it be done more easily? Can you form a rule? --------------------- b. (define x 100) (let ((x 3) (y (+ x 1))) (+ x y)) = 104 (define x 100) (let ((x 3)) (let ((y (+ x 1))) (+ x y))) = 7 --------------------- *** bindings are local --------------------- 2. BINDINGS ARE GONE AFTER LET ENDS (+ (let ((z (* 3.14 2.73))) (f z z)) z) error if no binding for z outside --------------------- *** most local binding is one in effect ---------------------- 3. CLOSEST SURROUNDING BINDING USED (define x 3) (let ((x (+ x 1))) (let ((x (* x x))) (let ((x (* x x))) (+ x x)))) = 512 (NOT 6, 8, or 64) ---------------------- want to have other ways to explain the evaluation of this so we develop pictures... ** Arrows Show how to trace bindings to values with arrows formal parameter ~~~> to argument var use ---> formal parameter (or let binding) ** Environments (terminology) Scheme keeps the values of variables in an *environment* This is a table ---------------------- ENVIRONMENTS Def: An environment is a mapping from variables to values. cons ~~> [primitive-procedure|...] length ~~> [procedure|(ls)|...|...] recipes ~~> [((ham-and-cheese...)...)] x ~~> [2] ----------------------- Can think of this as a function. Or as a set of bindings. ------------------- BINDINGS Def: a binding is an association of a name to a value. x ~~> [2] "x is bound to 2" made by either LET or an application of a LAMBDA (LET ((x 2)) (+ x x)) ((LAMBDA (x) (+ x x)) 2) Def: a variable in an expression is bound by LET or LAMBDA if it is in the list of variables following LAMBDA or LET Def: Such variables are called (formal) parameters (LAMBDA (x) (+ x y)) Def: a variable is free in an expression if it occurs outside of any LET or LAMBDA binding for it in that expression ---------------------- Environment is like a culture: tells you the meaning of various symbols (V sign, flag...) ------------ BOUND FREE (let ((z 4)) (add1 (+ z y))) (let ((z 4)) (+ z 1)) (LAMBDA (f y) (f a (f y z))) ((LAMBDA (f y) (f a (f y z))) f 3) ------------ note that f is both bound and free in the last example Show how to work above examples by keeping track of environments. Have them do some in groups. *** global --------------- GLOBAL VS. LOCAL ENVIRONMENT Def: The global environment gives meaning to standard Scheme procedures, and names you define. (define x 2) (define square (lambda (x) (* x x))) Env0: * ~~> [primitive-procedure|...|Env0] x ~~> [2] square ~~> [procedure|(x)|(* x x)|Env0] --------------- **** Initial global environment (the culture inherited from our parents) gives values to standard procedures like +, -, *, cons, car, cdr, .... but *not* to keywords like DEFINE, LET, LAMBDA, IF, ... **** (User) Global environment the initial global environment plus our definitions (additions to culture) symbols we invent When we use DEFINE, bind variable to a value in the global environment. *** Local environment --------------- Def: A local environment gives meaning to parameters bound by LET or LAMBDA and refers to parent environment otherwise Env0: + ~~> [primitive-procedure|...|Env0] ... (DEFINE x 2) (DEFINE square (LAMBDA (x) (* x x))) x ~~> [2] square ~~> [procedure|(x)|(* x x)|Env0] (square 3) Env1: (parent Env0) x ~~> [3] (* x x) ==> 9 Env0: + ~~> [primitive-procedure|...|Env0] ... x ~~> [2] square ~~> [procedure|(x)|(* x x)|Env0] x ==> 2 --------------- To evaluate the call to square, set up a local environment, Env1, and in it bind x to 3 When evaluation of the call is done, result is returned, and interpreter puts us back in Env0 (Env1 is no longer needed and so disappears) binding x to 3 in Env1 does not affect the global environment (Env0) like we formed a culture to study this, only sent the result back. try the following on the computer (let ((z 3.14159)) (+ (- z 1) z)) z ; gives error since z is defined locally, it does not affect the global environment in any way. (let ((+ cons)) (let ((z 3.14159)) (+ (- z 1) 2))) ; returns the improper list (2.15159 . 2) -------------------- LOCAL ENVIRONMENTS AND RECURSION (DEFINE sum (LAMBDA (ls) (IF (null? ls) 0 (+ (car ls) (sum (cdr ls)))))) (sum (list 1 2 3)) -------------------- Q: What are the bound variables? Draw the global environment (Env0), with +, null?, car, cdr, sum Trace this with the local environments. *** Nesting of environments ------------------- NESTING OF ENVIRONMENTS (define x 2) (define square (lambda (x) (let ((result (* x x))) result))) Env0: * ~~> [primitive-procedure|...] x ~~> [2] square ~~> [procedure|(x)|(let ...)|Env0] (square 3) Env1: (parent Env0) x ~~> [3] (let ((result (* x x))) result) (* x x) ==> 9 Env2: (parent Env1) result ~~> [9] result ==> 9 ------------------- Show how * and x are evaluated. *** Scope of a binding ------------------- SCOPE OF A BINDING def: The scope of a binding of var to val is the area of the program in which that binding is in effect. (define x 2) x ; scope of x ~~> [2] (let ((y 3)) ; scope of y ~~> [3] (+ x y)) x ; scope of x ~~> [2] y ; error ; but not y ~~> [3] ------------------- This is the area of text where no nested environment binds var to val. Show with boxes, arrows Give examples, with diagrams (arrows) **** Hole in the scope Area of text where another binding for the variable is in effect ------------------- (let ((x 4)) ; scope of x ~~> [4] ; hole in scope of x ~~> [2] ...) ----------- type these in ... (let ((a 5)) (begin (writeln a) (let ((a 3)) (writeln a)) a)) (DEFINE a 10) (DEFINE b 2) (let ((a (+ a 5))) (* a b)) ** Closures type these in... (DEFINE add2 (let ((two 2)) (LAMBDA (x) (+ x two)))) (let ((two 100000)) (add2 25)) (DEFINE two two) (DEFINE add2 (LAMBDA (x) (+ x two))) returns 125 look up binding of h based on program text where defined Why? so you can remember what a procedure does based on name, not where it is used (dynamic scoping would give 100025) **** Lexically scoped (Statically scoped) ------------------- LEXICAL SCOPING def: A language is lexically scoped if the scope of a binding is fixed before running the program. def: A language is dynamically scoped otherwise. What this means: procedures must remember the environment of their birth. Def: a closure is a procedure object together with the environment of its birth ------------------- Scheme is lexically scoped The Unix shell is dynamically scoped. Bindings of free variables in shell programs only are determined when run the program. The procedure object -- called a closure -- remembers the local environment in which it was created. This is like a salmon remembering the stream (environment) in which it was born, so that when it comes time for it to spawn, it returns to the environment of its birth. Example: draw arrows and trace this with envrionments, carefully. pay attention to closure saved env, and application parent env -------------- CLOSURE EXAMPLE Env0 / ~~> [procedure|(x y)|...|Env0] + ~~> [procedure|(x y)|...|Env0] (DEFINE add2 (LET ((two 2)) (LAMBDA (x) (+ x two)))) Env1: (parent Env0) two ~~> [2] (LAMBDA (x) (+ x two)) Env0: / ~~> [procedure|(x y)|...|Env0] + ~~> [procedure|(x y)|...|Env0] add2 ~~> [procedure|(x)|(+ x two)|Env1] (LET ((b 0.5)) (/ b (add2 b))) Env2: (parent Env0) b ~~> [0.5] (/ b (add2 b)) ------------- The point is that the closure mechanism allows us to: understand the definition, write down a description (specification) of that, use that description to reason about the closure. if the meaning of a LAMBDA depended on the context of its use, (dynamic scoping), it would be much harder to understand programs ** letrec *** problem: cannot use let to define recursive procedures locally ------------ CAN'T USE LET TO DEFINE RECURSIVE PROCS (let ((n-dup (LAMBDA (n x) (IF (zero? n) '() (cons x (n-dup (sub1 n) x)))))) (n-dup 2 b)) ------------ Q: what happens? Why? Show the arrows, environment This gives error, because n-dup is unbound. That is the right thing for (let ((x (+ x 1))) ...) but not in this case. Since Scheme does not know what we mean, have to have a form like let that handles this: ------------ LETREC SOLVES THIS PROBLEM (letrec ((n-dup (LAMBDA (n x) (IF (zero? n) '() (cons x (n-dup (sub1 n) x)))))) (n-dup 2 b)) ------------ LETREC is like LET but for recursion Changes scope to include right hand side. When would you want to have local recursive functions? Most of the time when you want to hide a helping proc that is recursive. In defining an iterative procedure, helper-it would be recursive, not needed outside. Many other situations where helpers are recursive. *** syntax --------------- SYNTAX OF LETREC (LETREC ((var1 val1) ... (varn valn)) body) ---------------- the values are nearly always procedures, value expressions must not directly refer to the other values, but ok (not direct) if inside a LAMBDA another example ------------ EXAMPLE ;; make the list ;; (4 (3 (2 (1 (0) 1) 2) 3) 4) (LETREC ((down-up ; TYPE: (-> (number) ((list number))) (LAMBDA (n) (IF (zero? n) (list 0) (list n (down-up (sub1 n)) n))))) (down-up 4)) ------------ Use append instead of list if want without sublists Draw arrows -------------- Env0: zero? ~~> [primitive-procedure|...] list ~~> [primitive-procedure|...] sub1 ~~> [procedure|(n)|(- n 1)|Env0] (LETREC ((down-up ...)) (down-up 4)) Env1: (parent Env0) down-up ~~> [procedure|(n)|(...)|Env1] (down-up 4) Env2: (parent Env1) n ~~> [4] (IF (zero? n) (list 0) (list n (down-up (sub1 n))) n) = (list n (down-up (sub1 n))) n) --------------- Trace the rest of the execution. (if time) Env3: (parent Env0) n ~~> [4] (- n 1) Env4: (parent Env1) n ~~> [3] (IF (zero? n) (list 0) (list n (down-up (sub1 n))) n) = (list n (down-up (sub1 n))) n) = (list 3 (list 2 (list 1 (list 0) 1) 2) 3) *** normal use Example of normal use: Generalizing the double-reverse-sublist ------------ USING LETREC TO SAVE PASSING ARGUMENTS (n-reverse-sublist 3 '((a b) (c d))) ==> ((b a) (b a) (b a) (d c) (d c) (d c)) (DEFINE n-reverse-sublist (LAMBDA (n ls) ; REQUIRES: n > 0 (LETREC ((n-dup-on ;: (-> (int datum list) list) (LAMBDA (n to-dup tail) ; REQUIRES: n > 0 ; ENSURES: result is n conses ; of to-dup in front of tail (IF (zero? n) tail (cons to-dup (n-dup-on (sub1 n) to-dup tail)))))) (IF (null? ls) '() (n-dup-on n (reverse (car ls)) (n-reverse-sublist n (cdr ls))))))) ------------ Try this Q: is it necesasry to pass n to n-dup-on and recursive calls? Why? have them think about this in groups for 1 min -- note that it is ok, but not necessary to have the n as parameter to both LAMBDAs -- note that subtracting 1 from n in n-dup-on does not change the outer n, as that is local, and does not change the recursive call To avoid the passing of arguments that do not change -- like to-dup, tail in n-reverse-sublist, and n in n-reverse-sublist bind them outside. THIS IS A MAJOR USE OF LET/LETREC!! ------------ AVOIDING PASSING UNCHANGING ARGUMENTS (DEFINE n-reverse-sublist2 (LAMBDA (n ls) ; REQUIRES: n > 0 (LET ((n-dup-on ;:(-> (datum list) list) (LAMBDA (to-dup tail) ; ENSURES: result is n conses ; of to-dup in front of tail (LETREC ((n-dup ;: (-> (num) list) (LAMBDA (n*) (IF (zero? n*) tail (cons to-dup (n-dup (sub1 n*))))))) (n-dup n))))) (LETREC ((helper ; TYPE: (-> (list) list) (LAMBDA (ls) (IF (null? ls) '() (n-dup-on (reverse (car ls)) (helper (cdr ls))))))) (helper ls))))) ------------ Lots of you wanted Scheme to do this for you, indicates how much more readable this makes your programs.