2013-03-01 - "Compilation" and Partial Evaluation (contd.) - Lazy Evaluation: Using a Lazy Racket - Lazy Evaluation: Some Issues ======================================================================== So far, nothing much changed. We curried the `eval' function and renamed it to `compile'. But when we actually call compile, then almost nothing happens -- all it does is create a Racket closure which will do the rest of the work. (And this closure closes over the given expression.) Running this "compiled" code is going to be very much like the previous usage of `eval', except a little *slower*, because now every recursive call involves calling `compile' to generate a closure, which is then immediately used -- so we just added some allocations at the recursive call points! (Actually, the extra cost is minimal because the Racket compiler will optimize away such immediate closure applications.) However, the conceptual change is substantial -- we now have a function that does its work in two stages -- the first part gets an expression and *can* do some compile-time work, and the second part does the run-time work, and includes anything inside the (lambda (env) ...). The thing is that so far, the code does nothing at the compilation stage (remember: only creates a closure). But because we have two stages, we can now shift work from the second stage (the run-time) to the first (the compile-time). For example, consider the following simple example: #lang pl (: foo : Number Number -> Number) (define (foo x y) (* x y)) (: bar : Number -> Number) (define (bar c) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ (foo c n) acc)) acc)) (loop 40000000 0)) (time (bar 0)) We can do the same thing here -- separate `foo' it into two stages using currying, and modify `bar' appropriately: #lang pl (: foo : Number -> Number -> Number) (define (foo x) (lambda (y) (* x y))) (: bar : Number -> Number) (define (bar c) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ ((foo c) n) acc)) acc)) (loop 40000000 0)) (time (bar 0)) Now instead of a simple multiplication, lets expand it a little, for example, do a case split on common cases where x is 0, 1, or 2: (: foo : Number -> Number -> Number) (define (foo x) (lambda (y) (cond [(= x 0) 0] [(= x 1) y] [(= x 2) (+ y y)] ; assume that this is faster [else (* x y)]))) This is not much faster, since Racket already optimizes multiplication in a similar way. Now comes the real magic: deciding what branch of the cond to take depends *only* on x, so we can `push' the lambda inside: (: foo : Number -> Number -> Number) (define (foo x) (cond [(= x 0) (lambda (y) 0)] [(= x 1) (lambda (y) y)] [(= x 2) (lambda (y) (+ y y))] [else (lambda (y) (* x y))])) We just made an improvement -- the comparisons for the common cases are now done as soon as (foo x) is called, they're not delayed to when the resulting function is used. Now go back to the way this is used in `bar' and make it call `foo' once for the given `c': #lang pl (: foo : Number -> Number -> Number) (define (foo x) (cond [(= x 0) (lambda (y) 0)] [(= x 1) (lambda (y) y)] [(= x 2) (lambda (y) (+ y y))] [else (lambda (y) (* x y))])) (: bar : Number -> Number) (define (bar c) (define foo-c (foo c)) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ (fooc n) acc)) acc)) (loop 40000000 0)) (time (bar 0)) Now foo-c is generated once, and if `c' happens to be one of the three common cases (as in the last expression), we can avoid doing any multiplication. (And if we hit the default case, then we're doing the same thing we did before.) [However, the result runs a little slower! The reason is that dealing with functions can have a higher cost when the compiler cannot "simplify closures away" -- and this is what happens in the last version. The additional overhead is much higher than the multiplication we save (the Racket compiler inlines multiplications, so their cost is close to just executing a single machine-code instruction).] Here is another useful example that demonstrates this: (define (foo list) (map (lambda (n) (if ...something... E1 E2)) list)) --> (define (foo list) (map (if ...something... (lambda (n) E1) (lambda (n) E2)) list)) (Question: when can you do that?) This is not unique to Racket, it can happen in any language. Racket (or any language with first class function values) only makes it easy to create a local function that is specialized for the flag. ======================================================================== Getting our thing closer to a compiler is done in a similar way -- we push the `(lambda (env) ...)' inside the various cases. (Note that `compile*' depends on the `env' argument, so it also needs to move inside -- this is done for all cases that use it, and will eventually go away.) We actually need to use `(lambda: ([env : ENV]) ...)' though: (: compile : TOY -> ENV -> VAL) ;; compiles TOY expressions to Racket functions. (define (compile expr) (cases expr [(Num n) (lambda: ([env : ENV]) (RktV n))] [(Id name) (lambda: ([env : ENV]) (lookup name env))] [(Bind names exprs bound-body) (lambda: ([env : ENV]) (: compile* : TOY -> VAL) (define (compile* expr) ((compile expr) env)) ((compile bound-body) (extend names (map compile* exprs) env)))] [(Fun names bound-body) (lambda: ([env : ENV]) (FunV names bound-body env))] [(Call fun-expr arg-exprs) (lambda: ([env : ENV]) (: compile* : TOY -> VAL) (define (compile* expr) ((compile expr) env)) (let ([fval (compile* fun-expr)] [arg-vals (map compile* arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) ((compile body) (extend names arg-vals fun-env))] [else (error 'call ; this is *not* a compilation error "function call with a non-function: ~s" fval)])))] [(If cond-expr then-expr else-expr) (lambda: ([env : ENV]) (: compile* : TOY -> VAL) (define (compile* expr) ((compile expr) env)) (compile* (if (cases (compile* cond-expr) [(RktV v) v] ; Racket value => use as boolean [else #t]) ; other values are always true then-expr else-expr)))])) and we have just shifted a some more work to compile time -- the code that checks what structure we have, and extracts its different slots. But this is still not good enough -- it's only the first top-level `cases' that is moved to compile-time -- recursive calls to `compile' are still there in the resulting closures. This can be seen by the fact that we have calls to `compile' in the Racket closures that are the results of our compiler: this compiler is essentially cheating by including calls to the compiler in the compiled results. For example, consider the `Bind' case: [(Bind names exprs bound-body) (lambda: ([env : ENV]) (: compile* : TOY -> VAL) (define (compile* expr) ((compile expr) env)) ((compile bound-body) (extend names (map compile* exprs) env)))] At compile-time we identify and deconstruct the Bind structure, then create a closure that has easy access to these parts. But this closure will itself call `compile' on `bound-body' and each of the expressions. Both of these calls can be done at compile time, since they only need the expressions, not the environment. Note that `compile*' turns to `run' here, since all it does is to run a compiled expression on the current environment. [(Bind names exprs bound-body) (let ([compiled-body (compile bound-body)] [compiled-exprs (map compile exprs)]) (lambda: ([env : ENV]) (: run : (ENV -> VAL) -> VAL) (define (run compiled-expr) (compiled-expr env)) (compiled-body (extend names (map run compiled-exprs) env))))] [We can move it back up, out of the resulting functions, by making it a function that consumes an environment and returns a "run" function: (define (compile expr) ;; convenient helper (: run : ENV -> ((ENV -> VAL) -> VAL)) (define (run env) (lambda (compiled) (compiled env))) (cases expr ... [(Bind names exprs bound-body) (let ([compiled-body (compile bound-body)] [compiled-exprs (map compile exprs)]) (lambda: ([env : ENV]) (compiled-body (extend names (map (run env) compiled-exprs) env))))] ...)) ] We can deal in a similar way with other occurrences of `compile' calls in compiled code -- eventually reaching a point where `compile' scans the input expressions completely, and the resulting compiled result does not need to call `compile' again. In fact, the resulting compiled Racket closure does not need to refer back to the original expression at all. The two branches that need to be fixed are: 1. In the `If' branch, there is not much to do. After we make it pre-compile the `cond-expr', we also need to make it pre-compile both the `then-expr' and the `else-expr'. This might seem like doing more work (since before changing it only one would get compiled), but since this is compile-time work, then it's not as important. Also, `if' expressions are evaluated many times (being part of a loop, for example), so overall we still win. 2. The `Call' branch is a little trickier: the problem here is that the expressions that are compiled are coming from the closure that is being applied. The solution for this is obvious: we need to change the closure type so that it closes over *compiled* expressions instead of over plain ones. This makes sense because closures are run-time values, so they need to close over the compiled expressions since this is what we use as "code" at run-time. ======================================================================== >>> Lazy Evaluation: Using a Lazy Racket [[[ PLAI Chapter 7 (done with Haskell) ]]] For this part, we will use a new language, Lazy Racket. #lang pl lazy As the name suggests, this is a version of the normal (untyped) Racket language that is lazy. First of all, let's verify that this is indeed a lazy language: > (define (foo x) 3) > (foo (+ 1 "2")) 3 That went without a problem -- the argument expression was indeed not evaluated. In this language, you can treat all expressions as future `promises' to evaluate. There are certain points where such promises are actually `forced', all of these stem from a need to print a resulting value: > (+ 1 "2") +: expects type as 2nd argument, given: "2"; other arguments were: 1 The expression by itself only generates a promise, but when we want to print it, this promise is forced to evaluate -- this forces the addition, which forces its arguments (plain values rather than computation promises), and at this stage we get an error. (If we never want to see any results, then the language will never do anything at all.) So a promise is forced either when a value printout is needed, or if it is needed to recursively compute a value to print: > (* 1 (+ 2 "3")) +: expects type as 2nd argument, given: "3"; other arguments were: 2 Note that the error was raised by the internal expression: the outer expression uses `*', and `+' requires actual values not promises. Another example, which is now obvious, is that we can now define an `if' function: > (define (my-if x y z) (if x y z)) > (my-if (< 1 2) 3 (+ 4 "5")) 3 Actually, in this language `if', `and', and `or' are all function values instead of special forms: > (list if and or) (# # #) (By now, you should know that these have no value in Racket -- using them like this in plain will lead to syntax errors.) There are some primitives that do not force their arguments. Constructors fall in this category, for example `cons' and `list': > (define a (list 1 2 (+ 3 "4") (* 5 6))) Nothing -- the definition simply worked, but that's expected, since nothing is printed. If we try to inspect this value, we can get some of its parts, provided we do not force the bogus one: > (first a) 1 > (second a) 2 > (fourth a) 30 > (third a) +: expects type as 2nd argument, given: "4"; other arguments were: 3 The same holds for cons: > (second (cons 1 (cons 2 (first null)))) 2 Now if this is the case, then how about this: > (define ones (cons 1 ones)) Everything is fine, as expected -- but what is the value of `ones' now? Clearly, it is a list that has 1 as its first element: > (first ones) 1 But what do we have in the tail of this list? We have `ones' which we already know is a list that has 1 in its first place -- so following Racket's usual rules, it means that the second element of `ones' is, again, 1. If we continue this, we can see that `ones' is, in fact, an *infinite* list of 1s: > (second ones) 1 > (fifth ones) 1 In this sense, the way `define' behaves is that it defines a true equation: if ones is defined as (cons 1 ones), then the real value does satisfy (equal? ones (cons 1 ones)) which means that the value is the fixpoint of the defined expression. We can use `append' in a similar way: > (define foo (append (list 1 2 3) foo)) > (fourth foo) 1 This looks like it has some common theme with the discussion of implementing recursive environments -- it actually demonstrates that in this language, `letrec' can be used for "simple" values too. First of all, a side note -- here an expression that indicated a bug in our substituting evaluator: > (let ([x (list y)]) (let ([y 1]) x)) reference to undefined identifier: y When our evaluator returned `1' for this, we noticed that this was a bug: it does not obey the lexical scoping rules. As seen above, Lazy Racket is correctly using lexical scope. Now we can go back to the use of `letrec' -- what do we get by this definition: > (define twos (let ([xs (cons 2 xs)]) xs)) we get an error about `xs' being undefined. `xs' is unbound because of the usual scope that `let' uses. How can we make this work? -- We simply use `letrec': > (define twos (letrec ([xs (cons 2 xs)]) xs)) > (first twos) 2 As expected, if we try to print an infinite list will cause an infinite loop, which DrRacket catches and prints in that weird way: > twos #0=(2 . #0#) How would we inspect an infinite list? We write a function that returns part of it: > (define (take n l) (if (or (<= n 0) (null? l)) null (cons (first l) (take (sub1 n) (rest l))))) > (take 10 twos) (2 2 2 2 2 2 2 2 2 2) > (define foo (append (list 1 2 3) foo)) > (take 10 foo) (1 2 3 1 2 3 1 2 3 1) Dealing with infinite lists can lead to lots of interesting things, for example: > (define fibs (cons 1 (cons 1 (map + fibs (rest fibs))))) > (take 10 fibs) (1 1 2 3 5 8 13 21 34 55) Too see how it works, see what you know about `fibs[n]' which will be our notation for the nth element of `fibs' (starting from 1): fibs[1] = 1 because of the first `cons' fibs[2] = 1 because of the second `cons' and for all n>2: fibs[n] = (map + fibs (rest fibs))[n-2] = fibs[n-2] + (rest fibs)[n-2] = fibs[n-2] + fibs[n-2+1] = fibs[n-2] + fibs[n-1] so it follows the exact definition of Fibonacci numbers. ======================================================================== >>> Lazy Evaluation: Some Issues There are a few issues that we need to be aware of when we're dealing with a lazy language. First of all, remember that our previous attempt at lazy evaluation has made {with {x y} {with {y 1} x}} evaluate to 1, which does not follow the rules of lexical scope. This is *not* a problem with lazy evaluation, but rather a problem with our naive implementation. We will shortly see a way to resolve this problem. In the meanwhile, remember that when we try the same in Lazy Racket we do get the expected error: > (let ([x y]) (let ([y 1]) x)) reference to undefined identifier: y A second issue is a subtle point that you might have noticed when we played with Lazy Racket: for some of the list values we have see a "." printed. This is part of the usual way Racket displays an "improper list" -- any list that does not terminate with a null value. For example, in plain Racket: > (cons 1 2) (1 . 2) > (cons 1 (cons 2 (cons 3 4))) (1 2 3 . 4) In the dialect that we're using in this course, this is not possible. The secret is that the `cons' that we use first checks that its second argument is a proper list, and it will raise an error if not. So how come Lazy Racket's `cons' is not doing the same thing? The problem is that to know that something is a proper list, we will have to force it, which will make it not behave like a constructor. [As a side note, we can achieve some of this protection if we don't insist on immediately checking the second argument completely, and instead we do the check when needed -- lazily: (define (safe-cons x l) (cons x (if (list? l) l (error "poof")))) ] Finally, there are two consequences of using a lazy language that make it more difficult to debug (or at lease take some time to get used to). First of all, control tends to flow in surprising ways. For example, enter the following into DrRacket, and run it in the normal language level for the course: (define (foo3 x) (/ x "1")) (define (foo2 x) (foo3 x)) (define (foo1 x) (list (foo2 x))) (define (foo0 x) (first (foo1 x))) (+ 1 (foo0 3)) In the normal language level, we get an error, and red arrows that show us how where in the computation the error was raised. The arrows are all expected, except that `foo2' is not in the path -- why is that? Remember the discussion about tail-calls and how they are important in Racket since they are the only tool to generate loops? This is what we're seeing here: `foo2' calls `foo3' in a tail position, so there is no need to keep the `foo2' context anymore -- it is simply replaced by `foo3'. Now switch to Lazy Racket and re-run -- you'll see a single arrow, skipping over everything and going straight to the erroneous expression. What's the problem? The call of `foo0' creates a promise that is forced in the top-level expression, that simply returns the `first' of the `list' that `foo1' created -- and all of that can be done without forcing the `foo2' call. Going this way, the computation is finally running into an error *after* the calls to `foo0', `foo1', and `foo2' are done -- so we get the seemingly out-of-context error. Finally, there are also potential problems when you're not careful about memory use. A common technique when using a lazy language is to generate an infinite list and pull out its Nth element. For example, to compute the Nth Fibonacci number, we've seen how we can do this: (define fibs (cons 1 (cons 1 (map + fibs (rest fibs))))) (define (fib n) (list-ref fibs n)) and we can also do this (reminder: `letrec' is the same as an internal definition): (define (fib n) (letrec ([fibs (cons 1 (cons 1 (map + fibs (rest fibs))))]) (list-ref fibs n))) but the problem here is that when `list-ref' is doing its way down the list, it might still hold a reference to `fibs', which means that as the list is forced, all intermediate values are held in memory. In the first of these two, this is guaranteed to happen since we have a binding that points at the head of the `fibs' list. With the second form things can be confusing: it might be the case that our language implementation is smart enough to see that `fibs' is not really needed any more and release the offending hold, but even if this is the case, tricky situations are hard to avoid. (Side note: Racket didn't use to do this optimization, but now it does.) This is also tricky no matter how smart the compiler is. For example, compare this: (define (foo) (define nats1 (cons 1 (map add1 nats1))) (define nats2 (cons 1 (map add1 nats2))) (or (equal? nats1 nats2) (error "the list that begins with ~s is bad" (first nats1)))) to this: (define (foo) (define nats1 (cons 1 (map add1 nats1))) (define nats2 (cons 1 (map add1 nats2))) (let ([fst (first nats1)]) (or (equal? nats1 nats2) (error "the list that begins with ~s is bad" fst)))) ========================================================================