Final Exam Friday, December 14 10:30-12:30 AM Open-book, open-notes, no computers Review and sample questions =========================== Notes: Everything on the mid-term (and in the mid-term review) still applies. ">" in left--hand column marks sample questions. Recursive data definitions and recursive functions -------------------------------------------------------- ; A foobar is ; * 7, or ; * (list n f 9) ; where n is a number and f is a foobar = 7 | (list 9) > Which of these expressions produce s? * 7 * '(0 7 9) * (cons 0 (cons 7 (cons 0 '()))) * (list 0 7 9) * '(9 7 0) * '(0 7 . 9) * (cons 0 (cons 7 9)) * '(0 (0 (0 (0 7 9) 9) 9) 9) > Provide a proof tree showing that (cons 1 (cons 7 7 9) 9) is a . 7 is in 7 is in -------------------------------- (list 7 7 9) is in 1 is in -------------------------------------------------- (list 1 (list 7 7 9) 9) is in [The answer MUST have the above shape.] Functions that consume s: ; F : foobar -> ... (define (F fb) (cond [(eq? fb 7) ...] [else ... (car fb) ... (F (cadr fb)) ...])) - F must have a "cond" expression - The "cond" expression must have two cases - The first case must recognize an instance of the first case in the data definition - Each case should extract (non-constant) members of fb - Data members of type will likely be passed to F > Implement "sum", which takes a and returns the sum of all numbers in the . ; sum : foobar -> num < CONTRACT ; Adds up all the numbers in fb. < PURPOSE ; (sum '7) = 7 < EXAMPLES ; (sum (list 12 7 9)) = 28 (one for each case) (define (sum fb) < IMPLEMENTATION (cond (follows shape) [(eq? fb 7) 7] [else (+ (car fb) (sum (cadr fb)) 9)])) Mutually recursive data definitions imply mutually recursive procedures: = 1 | (cons 2 ) = (cons ) ; Fa : a -> ... (define (Fa a) (cond [(eq? a 1) ...] [else ... (Fb (cdr a)) ...])) ; Fb : b -> ... (define (Fb b) ; Only one case: ... (Fa (car b)) ... (Fa (cdr b)) ...) > Implement the function "sum", which takes an and returns the sum of all numbers in the . ... like above; needs CONTRACT, PURPOSE, and EXAMPLES ... Lexical scope --------------------------------------------------------- Assuming our proc language, with `let' and `letrec': - Free variables: x is free in E if * E = y and x = y * E = proc(y1, ...)E2 and y1 != x AND ... AND x is free in E2 * E = (E1 E2) and x is free in E1 OR x is free in E2 * E = prim(E1, ..) and x is free in E1 OR ... * E = let y1 = E1 ... in Eb and x is free in E1 OR ... OR y1 != x AND ... AND x is free in Eb * E = letrec y1 = E1 ... in Eb and y1 != x AND ... AND x is free in E1 OR ... OR x is free in Eb * E = if E1 then E2 else E3 and x is free in E1 OR ... - Bound variables: x is bound in E if * E = proc(y1, ...)E2 and y1 = x OR ... AND x is free in E2 OR x is bound in E2 * E = (E1 E2) and x is bound in E1 OR x is bound in E2 * E = prim(E1, ..) and x is bound in E1 OR ... * E = let y1 = E1 ... in Eb and x is bound in E1 OR ...x is bound in Eb OR y1 = x OR ... AND x is free in Eb * E = letrec y1 = E1 ... in Eb and x is bound in E1 OR ... OR x is bound in Eb OR y1 = x OR ... AND x is free in E1 OR ... OR x is free in Eb * E = if E1 then E2 else E3 and x is bound in E1 OR ... A _binding occurrence_ is a variable in a "proc" argument list or a "let"/"letrec" left-hand side which causes a variable to be bound. > Given the expression let g = proc(n)+(n,6) y = g in let x = r y = g in letrec f = proc(z)(f -(z,x)) in (f (y 9)) Draw arrows from bound variable to binding occurrences. .------------------. v v---. | let g = proc(n)+(n,6) | y = g .-----' in let x = r <-/---------------. ,---> y = g -' v------. \ / in letrec f = proc(z)(f -(z, x)) \ ,^-----------' `----------|--. in (f (y 9)) > List the free vars: g, r > List the bound vars: g, n, x, y, f, z Variable names are interchangeable, as long as a binding occurrence and its bound references are changed consistently. Renaming every binding instance in the above program to a new, unique name, and update all bound occurrences to produce an equivalent program: let g1 = proc(n1)+(n1,6) y1 = g in let x2 = r y2 = g1 in letrec f3 = proc(z4)(f3 -(z4,x2)) in (f3 (y2 9)) In fact, we can eliminate name at references altogether by using lexical addresses: @(1, 3) ^ ^--- means "the 4th binding in the set" `--- means "cross two contours" > Show the same program as above using lexical addresses: let g1 = proc(n1)+(@(0,0),6) y1 = g in let x2 = r y2 = @(0,0) in letrec f3 = proc(z4)(@(1,0) -(@(0,0),@(2,0))) in (@(0,0) (@(1,1) 9)) Scheme Reductions --------------------------------------------------------- Using the following subset of Scheme: = | (let ( ) ) | (lambda () ) | ( ) = (lambda () ) > Show the evaluation of (let (f (lambda (x) x)) ((f f) (lambda (y) y))) to a value: (let (f (lambda (x) x)) ((f f) (lambda (y) y))) -> (((lambda (x) x) (lambda (x) x)) (lambda (y) y)) -> ((lambda (x) x) (lambda (y) y)) -> (lambda (y) y) Interpreters for a functional language --------------------------------------------------------- + Lexically scoped languages The language grammar from above: = | (let ( ) ) | (lambda () ) | ( ) An interepreter takes an (or its representation in abstract syntax) and produces the result of evaluating the expression. An interpreter's "eval" is just another recursive function. (define (eval e) (cond [(symbol? e) ...] [(let? e) ... (eval (cadr (cadar e))) ... (eval (caddr e)) ...] [(lambda? e) ... (eval (caddr e)) ...] ; << ! [else ... (eval (car e)) ... (eval (cadr e)) ...])) It turns out that we don't want the recursive call for the `lambda?' case, though: (define (eval e) (cond [(symbol? e) ...] [(let? e) ... (eval (cadr (cadar e))) ... (eval (caddr e)) ...] [(lambda? e) ... (closure (cadar e) (caddr e)) ...] [else ... (eval (car e)) ... (eval (cadr e)) ...])) To implement lexical scope, the "eval" function typically accumulates information in an environment: (define (eval e env) (cond [(symbol? e) (lookup env e)] [(let? e) (let ([v (eval (cadr (cadar e)) env)]) (eval (caddr e) (extend env (car (cadar e)) v)))] [(lambda? e) (closure (cadar e) (caddr e) env)] [else ... (eval (car e) env) ... (eval (cadr e) env) ...])) The application part expects a closure as the result of the first expression: (define (eval e env) (cond [(symbol? e) (lookup env e)] [(let? e) (let ([v (eval (cadr (cadar e)) env)]) (eval (caddr e) (extend env (car (cadar e)) v)))] [(lambda? e) (closure (cadar e) (caddr e) env)] [else (let ([f (eval (car e) env)] [a (eval (cadr e) env)]) (apply f a))]) (define (apply f a) (let ([id (closure->id f)] [body (closure->body f)] [env (closure->env f)]) (eval body (extend env id a)))) > Given the expression: (let (f (lambda (y) y)) (lambda (x) f)) - Describe the closure bound to f at the point where "(lambda (x) f)" is the current expression: ^ ^ ^--- env | `--- body expr `--- formal argument - Describe the environment at the point where "(lambda (x) f)" is the current expression: { f = , {} } (The final will more likely ask such a question in terms of the proc language.) (let (f (lambda (y) y)) (f (lambda (x) x))) What does the environmentlook like when then current expression is `y'? { y = , {}} > , {} } + Assignment In the basic language, the environment maps variables to values. - Expresed values: results returned by an expresion - Denoted values: meaning of a variable In other words, in the basic language, the set of expressed and denoted values is the same: Expressed values = procedures [, numbers, booleans] Denoted values = procedures [, numbers, booleans] When we add assignment --- e.g., set x = +(1,2) --- then the environment maps variables to *locations* and a location contains a value that can be replaced with a different one. Expressed values = procedures [, numbers, booleans] Denoted values = locations (holding expressed vals) + Parameter-passing Assignment exposes the order of evaluation. - call-by-value: arguments evaluated before procedure bodies - call-by-name: expression for an argument evaluated at the point where the argument is used - call-by-need: like call-by-name, but remember the result, in case the variable is used again Implementation technique for call-by-name and call-by-need: thunks = expr + env. > Given the following expression let x = 10 in let f = proc(y, z) { set x = +(y, y); z } in (f { set x = +(x, 1); x } x) what is its value in * call-by-value? 11 * call-by-name? 23 * call-by-need? 22 + Continuations There's more to evaluation order than just arguments: how do recursive evaluations of sub-expression continue with the rest of the evaluation? A continuation, like a to-do list, exposes the staging of evaluation needed to recur on subexpressions. The "eval" function takes three arguments: expression, envrionment, and continuation. When it arrives at a value, it applies the continaution. When it starts evaluations a subexpression, it extends the continuation. (define (eval e env cont) (cond [(symbol? e) (apply-cont cont (lookup env e))] [(let? e) (eval (cadr (cadar e)) env (let-cont (cadar e) env (caddr e) cont))] [(lambda? e) (apply-cont cont (closure (cadar e) (caddr e) env))] [else (eval (car e) env (apparg-cont (cadr e) env cont))])) (define (apply-cont v cont) (cond [(done-cont? cont) v] [(let-cont? cont) (eval (let-cont->exp cont) (extend (let-cont->env cont) (let-cont->id cont) v) (let-cont->oldcont cont))] [(apparg-cont? cont) (eval (apparg-cont->argexp cont) (apparg-cont->env cont) (app-cont v (apparg-cont->oldcont cont)))] [(app-cont? cont) (apply (app-cont->proc cont) v (app-cont->oldcont cont))])) (define (apply f a cont) (let ([id (closure->id f)] [body (closure->body f)] [env (closure->env f)]) (eval body (extend env id a) cont))) At this point, every call to "eval" or "apply-cont" or "apply" produces the final result, so no context is hidden in the Scheme implementation. > By showing every call to "eval" and "apply-cont", show completely how the following expresion is evaluated: (let (f (lambda (x) x)) ((f f) (lambda (y) y))) expr= (let (f (lambda (x) x)) ((f f) (lambda (y) y))) env= {} cont= [done] expr= (lambda (x) x) env= {} cont [let f ((f f) (lambda (y) y)) {} [done]] val= cont= [let f ((f f) (lambda (y) y)) {} [done]] expr= ((f f) (lambda (y) y)) env= {f=, {}} cont= [done] expr= (f f) env= {f=, {}} cont= [apparg (lambda (y) y) {f=, {}} [done]] expr= f env= {f=, {}} cont= [apparg f {f=, {}} [apparg (lambda (y)y) {f=, {}} [done]]] val= cont= [apparg f {f=, {}} [apparg (lambda (y)y) {f=, {}} [done]]] expr= f env= {f=, {}} cont= [app [apparg (lambda (y)y) {f=, {}} [done]]] val= cont= [app [apparg (lambda (y)y) {f=, {}} [done]]] expr= x env= {x=, {}} cont= [apparg (lambda (y)y) {f=, {}} [done]] val= cont= [apparg (lambda (y)y) {f=, {}} [done]] expr= (lambda (y) y) env= {f=, {}} cont= [app [done]] val= , {}}> cont= [app [done]] expr= x env= {x=, {}}>, {}} cont= [done] val= , {}}> cont= [done] + Garbage collection Every closure creation, environment extension, or continuation extension must allocate memory. That memory is never explicitly freed. A garbage collector inspects the heap, discovers records that certainly won't be used in the future, and frees them. A two-space collector frees records implcitly by copying all the records that need to be kept to a new space. Since it moves records one-by-one to the first free spot in the new space, it also defragments memory. > Suppose an interepreter needs the following kinds of records managed by a two-space collector: tag 1: an integer tag 2: two pointers It has three registers and a main memory of size 32, so that to-space is of size 16. Here is the state of the machine just before a garbage collection: Reg 1: 4 Reg 2: 6 Reg 3: 14 1 3 1 5 1 7 2 0 4 2 2 0 1 8 1 19 <- Content 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <- Address ^ ^ ^ ^ ^ ^ ^ <- Records 7 0 2 5 <- Forwards Show the registers and new to-space after a collection. 1 7 2 7 0 1 19 1 3 Reg 1: 0 Reg 2: 2 Reg 3: 5 1 7 2 7 0 1 19 1 3 0 0 0 0 0 0 0 <- Content 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <- Address ^ ^ ^ ^ 4 6 14 0 <- Was Types --------------------------------------------------------- The goal of a type system is to reject programs that the interpreter doesn't want to deal with. Such programs include uses of "if" that use a number for the test instead of a boolean, or function applications that try to use a number as a function. For a practical language, the type system will have to reject programs that work, as well as programs that don't. That's an unavoidable cost of type checking. The benefit is that a programmer (and a compiler) know that certain things can't happen at run-time. Since the goal of type-checking is to prove that certain things never happen, type judgements must be carefully and completely justified --- even to the point of mentioning that "4" obviously has type "int". Type rules: G |- : int for any , any env G |- false : bool G |- true : bool G |- x : T if x:T is in G G |- E1 : bool G |- E2 : T G |- E3 : T ------------------------------------------- G |- if E1 then E2 else E3 : T (for the some T) G, x:T |- E : T' ------------------------------------ G | - proc(T x)E : (T -> T') G |- E1 : (T -> T') G |- E2 : T ------------------------------------- G |- (E1 E2) : T' > Find and a type for the following expression (where "i" abbreviates int" and "b" abbreivates "bool"), and provide a proof tree demonstrating the type: proc((i -> b) x)if (x 10) then x else proc(i y)false {x:(i -> b), y:i} |- false : b ________________________________________ G1 |- proc(i y)false : (i -> b) \ \_________ G1 |- x : (i -> b) G1 |- 10 : i \ --------------------------------- \ G1 |- (x 10) : b G1 |- x : (i -> b) \ ----------------------------------------------------------- G1 = {x:(i -> b)} |- if (x 10) then x else proc(i y)false : (i -> b) ------------------------------------------------------------ {} |- proc((i -> b) x)if (x 10) then x else proc(i y)false : ((i ->b) -> (i -> b)) Objects --------------------------------------------------------- + Interpreter An object consists of a class tag, plus a value for each field declared/inherited by the class. In our language, every field is initialized with 0. In the case of a flat object representation, the field values are stored in an vector. The fields are ordered so that an instance of a class C --- whether created by "new C" or "new C2" for a class C2 derived from C --- always has the value of a certain field in C at a particular location in its vector. class c extends object field a % always at vector position 0 method initialize() a = 17 class cx extends c field b % always at vector position 1 field c % always at vector position 2 method m(w) begin c = b; b = w; c end class cy extends c field d % always at vector position 1 method m(w) begin d = a; a = w; d end let ox = new cx() oy = new cy() in begin send ox m(send oy m(2)); 7 end > Describe the object bound to ox at the point where "7" is the current expression. class tag: cx field vector: 17 17 0 When an object method is called, a new environment is constructed for evaluating the method body. The environment contains "self", "%super", and a binding for each field in the object, plus a binding for each method argument. The fields are ordered in the environment so that derived class fields can hide superclass fields using the same name. > Describe the current environment at the point where "d" (in the "m" method of "cy") is the current expression. env: w= 2 self= object: class tag: cy fields: ------------. %super= 'c | extends | d= 17 <--- array for the object ---' a= 2 extends empty-env For normal method calls, the interpreter consults the tag of the receiver object, then looks for the method starting with that class in the inheritance tree. For super method calls, the interpreter ignores the tag on the object and uses the tag bound to '%super. > What is the value of the following program? class c1 extends object method initialize() method m() 1 class c2 extends c1 method m() 10 method n1() send self m() method n2() super m() class c3 extends c2 method m() 100 let o = new c3() in +(send o n1(), send o n2()) % The result is 101, because n1 uses the m % in c3, while n2 uses the m in c1. + Types To type-check a program using objects, class names are used as types. A class as a type corresponds to an instance of the class (or one of its subclasses). To find the type of a program's expression, first build a picture of the class hierarchy, then check the program expression using that information. > What is the type (if any) of the following program? class c extends object method int initialize(int x) x new c(5) % The type is "c" > What is the type (if any) of the following program? class c extends object method int initialize(int x) x new c(new c(10)) % No type, because the initialization % method, called by "new", wants an "int" % instead of a "c".