Exercise 1 : Creating Closures and Environments. [Talk this one through as a class, but wait for answers from more than the ones who know what's going on] Consider our definition of larger-than using filter ;; larger-than : num list[num] -> list[num] ;; extract list of nums in given list that are larger than given num (define (larger-than anum alon) (filter (lambda (num) (> num anum)) alon)) ;; filter : (a -> bool) list[A] -> list[A] ;; extracts list of elts from input list that satisfy predicate (define (filter keep? alst) (cond [(empty? alst) empty] [(cons? alst) (cond [(keep? (first alst)) (cons (first alst) (filter keep? (rest alst)))] [else (filter keep? (rest alst))])])) - Assume I evaluate the expression (larger-than 5 (list 7 2 9 4 3)) What are the contents of the closure passed to filter? Ans: the env should contain both anum=5 and alon=(list 7 2 9 4 3). - How many closures get created while evaluating that expression? Ans: only one, since we evaluate the args to filter in larger-than before calling filter. - Look at the call (keep? (first alst)). The first time we reach this call, what environments are available (and what are their contents)? Ans: There are two: one inside the keep? closure that binds anum to 5 and alon to (list 7 2 9 4 3), and the current environment which binds keep? to the closure and alst to (list 7 2 9 4 3) - The second time we reach the call to keep? what environments are available and what are their contents? Ans: There are two: one inside the keep? closure that binds anum to 5 and alon to (list 7 2 9 4 3), and the current environment which binds keep? to the closure and alst to (list 2 9 4 3) - When we call keep and evaluate the (> anum num), which of the two environments mentioned above do you want to use and why? Ans: the one inside keep? because that's the only one that has a value for anum. - Which environment does the value of num come from? Ans: When we make the call to keep?, we add the binding num=7 to the environment. - To which environment? Ans: To the one inside keep?, that contained anum. Moral: when we call a function, we have to interpret its body in an environment that extends the environment stored in the closure with the arguments to the function. - Why do we need to use the environment that's stored in the closure? Ans: Because that's the environment that's guaranteed to have bindings for all of the variables that are in the scope of the function. (When we reuse variable names, as we did in the early closure examples, this is also how we guarantee that we get the bindings that were active when the function was created--ie, static scoping--rather than when the function was called.) Consider this point on the larger-than/filter program once more: the environment active when we evaluate filter only has bindings for the arguments to filter (since filter is defined at the top level). We need the environment that was active when we created the lambda in order to get bindings for anum. This is why we need closures -- because we are allowed to pass functions out of their scope, yet still want to refer to the variables that are in their scope. ----------------------------------------------------------------- Exercise 2: More practice with Closures: For each closure created while evaluating each of the following programs, indicate the contents of the environment for that closure. Use a format like (lambda (x) ...) has env (a = 1) (b = 9) --------- ((lambda (x) ((lambda () ((lambda (y) (lambda (w z) (+ w x y z))) 5)))) 4) Ans: (lambda (x) ...) has an empty environment (lambda () has env (x=4) (lambda (y) ...) has env (x=4) (lambda (w z) has env (x=4) (y=5) ---------- (define (func1 f x) (f (lambda (x) (+ x 5)) (* x x))) (func1 (lambda (func2 x) ((lambda (z) (func2 (+ x z))) 7)) 3) Ans: func1 is a lambda and has an empty environment (lambda (func2 x) ...) has an empty environment (lambda (x) ...) has env (f=closure with proc (lambda (func2 x) ...) and an empty env) (x=3) (lambda (z) ...) has env (func2=closure with proc (lambda (x) ...) and env (f=closure...) (x=3) (x=9) ----------------------------------------------------------------- Exercise 3: Reviewing Interpreters: An interpreter is a program that consumes a program and "runs" it. Why do we say that an interpreter "implements" a language? Because a language has two pieces: it's syntax (what you write down), and it's semantics (rules that tell you how the pieces of the syntax compute new information). Example: What does the following program do? (frob (bubble 5 9) (fribble 3)) Until I tell you what "frob", "bubble", and "fribble" do, you have no idea. That's what semantics gives you -- they tell you what operators and keywords in your language do. Question: How does an interpreter use the syntax and semantics? Ans: The interpreter implements the semantics. If I tell you that frob performs addition, your interpreter might include the line. [(frob? expr) (+ (frob-left expr) (frob-right expr))] Where's the syntax? That's what we create the expr data definition for: to capture the syntax as a data structure, so we can run the semantics (interpreter) to evaluate the program. Note: Implementing interpreters is useful because it forces you to understand the language. Notice how you couldn't implement call? without understanding closures. ----------------------------------------------------------------- Exericse 4: Implementing another interpreter There are 2 different versions of this exercise. Students who are struggling with this material should do the first. Anyone who understands the material up to now should try the second (which is substantially harder). ************************ Version 1: Consider the following language over words, with operators weave, palinize, concat, and replace (replace takes a 0-based position and a new letter, and replaces the contents of that position with that letter). Here are several examples of programs in the language and what they would return. [weave ace bdf] = abcdef [palinize ab] = abba [concat ace bdf] = acebdf [replace 1 g ace] = age [palinize [weave ad cc]] = acdccdca [concat [weave yy oo] [replace 3 d heat]] = yoyohead Implement an interpreter for this language (it's like the arithmetic interpreter we did in the first interpreter lecture). Here are some hints: - represent words as strings - DrScheme has functions string->list : string -> list[char] and list->string : list[char] -> string You'll want to use these in your implementation. Use helper functions where you need them. You won't have time to write all the helper functions, but write their contracts and purpose statements and then call them in your interpreter. ************************ Version 2: Awk is a program that parses data files according to regular expressions. We're going to implement a simplified version of awk. Here's our target program: (ITERATE (users empty) ("(.*):" (username) (cons username users)) (AFTER (display users))) Given this program and a file, awk would - initializes a variable users to empty - match the regular expression "(.*):" against each line of the file in turn and bind the part that matches to variable username - update the users variable (the same variable that we initialized) to the cons of the matched username onto users - write out the value of users after all lines have been processed. So, for example, running this program on a file containing Kathi:cs Oleg:wpi Dale:wpi Choong-Soo:cs Bin:cs would display a list of strings ("Kathi" "Oleg" "Dale" "Choong-Soo" "Bin") Implement an interpreter for awk. Your main program should take an awk program and a file as arguments. Model the file as a list of strings, where each string corresponds to a single line. The general form of an awk program is (ITERATE (VAR value) (match regexp (VAR ... VAR) CMD) (AFTER CMD)) (where the regexp would match a piece for each VAR in the list). You can limit your interpreter to the commands given in the same program. Scheme has a built-in regexp-match that you should use to implement this. Here's a sample call: > (regexp-match "(.*):" "kathi:cs") ("kathi:" "kathi") [Ignore the first arg; the second arg contains all characters up to the : in the string. The (.*) says "all characters" and the : says "stop when you reach :".]