#+TITLE: 4.2 - Variations on a Scheme — Lazy Evaluation #+STARTUP: indent #+OPTIONS: num:nil #+BEGIN_QUOTE Now that we have an evaluator expressed as a Lisp program, we can experiment with alternative choices in language design simply by modifying the evaluator. #+END_QUOTE Now that we're building our own scheme, we can try out alternate ways of implementing underlying language features, like changing order of evaluation, or how variables are bound. * COMMENT Set up source file #+BEGIN_SRC scheme :tangle yes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; 4.2 - Variations on a Scheme — Lazy Evaluation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (load "4-1.scheme") #+END_SRC * <<4.2.1>> Normal Order and Applicative Order #+BEGIN_QUOTE Scheme is an applicative-order language, namely, that all the arguments to Scheme procedures are evaluated when the procedure is applied. In contrast, normal-order languages delay evaluation of procedure arguments until the actual argument values are needed. Delaying evaluation of procedure arguments until the last possible moment (e.g., until they are required by a primitive operation) is called /lazy evaluation/. #+END_QUOTE From 1.1.5: #+BEGIN_QUOTE This alternative “fully expand and then reduce” evaluation method is known as normal-order evaluation, in contrast to the “evaluate the arguments and then apply” method that the interpreter actually uses, which is called applicative-order evaluation. #+END_QUOTE ** Exploiting lazy evaluation: ~Unless~ #+BEGIN_SRC scheme (define (unless condition usual-value exceptional-value) (if condition exceptional-value usual-value)) #+END_SRC #+BEGIN_QUOTE One can do useful computation, combining elements to form data structures and operating on the resulting data structures, even if the values of the elements are not known. #+END_QUOTE #+COMMENT: Find haskell article showing lazy evaluation ** Exercise 4.25 Suppose that (in ordinary applicative-order Scheme) we define ~unless~ as shown above and then define ~factorial~ in terms of ~unless~ as #+BEGIN_SRC scheme (define (factorial n) (unless (= n 1) (* n (factorial (- n 1))) 1)) #+END_SRC What happens if we attempt to evaluate ~(factorial 5)~? Will our definitions work in a normal-order language? ---------------------------------------------------------------------- Evaluating this with applicative-order, attempting to evaluate ~(factorial 5)~ would recurse indefinitely, as it continues to evaluate the recursion before reaching the terminating clause. With normal-order, the recursion wouldn't be evaluated unless ~(= n 1)~, so the call should terminate successfully. ** Exercise 4.26 Ben Bitdiddle and Alyssa P. Hacker disagree over the importance of lazy evaluation for implementing things such as ~unless~. Ben points out that it's possible to implement ~unless~ in applicative order as a special form. Alyssa counters that, if one did that, ~unless~ would be merely syntax, not a procedure that could be used in conjunction with higher-order procedures. Fill in the details on both sides of the argument. Show how to implement ~unless~ as a derived expression (like ~cond~ or ~let~), and give an example of a situation where it might be useful to have ~unless~ available as a procedure, rather than as a special form. ---------------------------------------------------------------------- #+COMMENT: This implementation intentionally left blank A situation where it could be useful to have ~unless~ available as a procedure would be if there was some need to pass it as an argument to some other method to parameterize flow control in a higher-order procedure. * <<4.2.2>> An Interpreter with Lazy Evaluation ** Modifying the evaluator *** Eval #+BEGIN_SRC scheme :tangle yes (define (eval exp env) (cond ((self-evaluating? exp) exp) ((variable? exp) (lookup-variable-value exp env)) ((quoted? exp) (text-of-quotation exp)) ((assignment? exp) (eval-assignment exp env)) ((definition? exp) (eval-definition exp env)) ((if? exp) (eval-if exp env)) ((lambda? exp) (make-procedure (lambda-parameters exp) (lambda-body exp) env)) ((begin? exp) (eval-sequence (begin-actions exp) env)) ((cond? exp) (eval (cond->if exp) env)) ((application? exp) (apply (actual-value (operator exp) env) (operands exp) env)) (else (error "Unknown expression type: EVAL" exp)))) #+END_SRC *** Apply #+BEGIN_SRC scheme :tangle yes (define (actual-value exp env) (force-it (eval exp env))) (define (apply procedure arguments env) (cond ((primitive-procedure? procedure) (apply-primitive-procedure procedure (list-of-arg-values arguments env))) ; changed ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) (extend-environment (procedure-parameters procedure) (list-of-delayed-args arguments env) ; changed (procedure-environment procedure)))) (else (error "Unknown procedure type -- APPLY" procedure)))) #+END_SRC *** Procedure Arguments #+BEGIN_SRC scheme :tangle yes (define (list-of-arg-values exps env) (if (no-operands? exps) '() (cons (actual-value (first-operand exps) env) (list-of-arg-values (rest-operands exps) env)))) (define (list-of-delayed-args exps env) (if (no-operands? exps) '() (cons (delay-it (first-operand exps) env) (list-of-delayed-args (rest-operands exps) env)))) #+END_SRC *** Conditionals #+BEGIN_SRC scheme :tangle yes (define (eval-if exp env) (if (true? (actual-value (if-predicate exp) env)) (eval (if-consequent exp) env) (eval (if-alternative exp) env))) #+END_SRC *** driver-loop #+BEGIN_SRC scheme :tangle yes (define input-prompt ";;; L-Eval input:") (define output-prompt ";;; L-Eval value:") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (actual-value input the-global-environment))) (announce-output output-prompt) (user-print output))) (driver-loop)) #+END_SRC ** Representing thunks Essentially, a delayed object *plus* an environment to evaluate it in. Memoization is achieved in ~force-it~ by changing the tag from ~thunk~ to ~evaluated-thunk~ the first time it is forced, saving the value, and discarding the environment. Subsequent calls to ~force-it~ will see the new tag, and simply return the stored value. #+BEGIN_SRC scheme :tangle yes (define (force-it obj) (if (thunk? obj) (actual-value (thunk-exp obj) (thunk-env obj)) obj)) (define (delay-it exp env) (list 'thunk exp env)) (define (thunk? obj) (tagged-list? obj 'thunk)) (define (thunk-exp thunk) (cadr thunk)) (define (thunk-env thunk) (caddr thunk)) (define (evaluated-thunk? obj) (tagged-list? obj 'evaluated-thunk)) (define (thunk-value evaluated-thunk) (cadr evaluated-thunk)) (define (force-it obj) (cond ((thunk? obj) (let ((result (actual-value (thunk-exp obj) (thunk-env obj)))) (set-car! obj 'evaluated-thunk) (set-car! (cdr obj) result) ; replace `exp' with its value (set-cdr! (cdr obj) '()) ; forget unneeded `env' result)) ((evaluated-thunk? obj) (thunk-value obj)) (else obj))) #+END_SRC ** Exercise 4.27 Suppose we type in the following definitions to the lazy evaluator: #+BEGIN_SRC scheme (define count 0) (define (id x) (set! count (+ count 1)) x) #+END_SRC Give the missing values in the following sequence of interactions, and explain your answers. #+BEGIN_QUOTE This exercise demonstrates that the interaction between lazy evaluation and side effects can be very confusing. This is just what you might expect from the discussion in *Note Chapter 3. #+END_QUOTE #+BEGIN_SRC scheme (define w (id (id 10))) #+END_SRC - #+BEGIN_SRC scheme ;;; L-Eval input: count ;;; L-Eval value: #+END_SRC - RESPONSE:: ~1~ The outer call to ~id~ is evaluated when passed to the primitive ~define~. The inner argument ~(id 10)~ is not evaluated at this time. - #+BEGIN_SRC scheme ;;; L-Eval input: w ;;; L-Eval value: #+END_SRC - RESPONSE:: ~10~ The ~id~ of ~10~ is ~10~. - #+BEGIN_SRC scheme ;;; L-Eval input: count ;;; L-Eval value: #+END_SRC - RESPONSE:: ~2~ Evaluating ~w~ forces its evaluation, which evaluates ~(id 10)~. This increments count again, changing its value to ~2~. ** Exercise 4.28 ~Eval~ uses ~actual-value~ rather than ~eval~ to evaluate the operator before passing it to ~apply~, in order to force the value of the operator. Give an example that demonstrates the need for this forcing. ** Exercise 4.29 Exhibit a program that you would expect to run much more slowly without memoization than with memoization. Also, consider the following interaction, where the ~id~ procedure is defined as in *Note Exercise 4-27:: and ~count~ starts at 0: #+BEGIN_SRC scheme (define (square x) (* x x)) #+END_SRC - #+BEGIN_SRC scheme ;;; L-Eval input: (square (id 10)) ;;; L-Eval value: #+END_SRC - #+BEGIN_SRC scheme ;;; L-Eval input: count ;;; L-Eval value: #+END_SRC Give the responses both when the evaluator memoizes and when it does not. ** Exercise 4.30 Cy D. Fect, a reformed C programmer, is worried that some side effects may never take place, because the lazy evaluator doesn't force the expressions in a sequence. Since the value of an expression in a sequence other than the last one is not used (the expression is there only for its effect, such as assigning to a variable or printing), there can be no subsequent use of this value (e.g., as an argument to a primitive procedure) that will cause it to be forced. Cy thus thinks that when evaluating sequences, we must force all expressions in the sequence except the final one. He proposes to modify ~eval-sequence~ from section *Note 4-1-1:: to use ~actual-value~ rather than ~eval~: #+BEGIN_SRC scheme (define (eval-sequence exps env) (cond ((last-exp? exps) (eval (first-exp exps) env)) (else (actual-value (first-exp exps) env) (eval-sequence (rest-exps exps) env)))) #+END_SRC a. Ben Bitdiddle thinks Cy is wrong. He shows Cy the ~for-each~ procedure described in *Note Exercise 2-23::, which gives an important example of a sequence with side effects: #+BEGIN_SRC scheme (define (for-each proc items) (if (null? items) 'done (begin (proc (car items)) (for-each proc (cdr items))))) #+END_SRC He claims that the evaluator in the text (with the original ~eval-sequence~) handles this correctly: #+BEGIN_SRC scheme ;;; L-Eval input: (for-each (lambda (x) (newline) (display x)) (list 57 321 88)) 57 321 88 ;;; L-Eval value: done #+END_SRC Explain why Ben is right about the behavior of ~for-each~. b. Cy agrees that Ben is right about the ~for-each~ example, but says that that's not the kind of program he was thinking about when he proposed his change to ~eval-sequence~. He defines the following two procedures in the lazy evaluator: #+BEGIN_SRC scheme (define (p1 x) (set! x (cons x '(2))) x) (define (p2 x) (define (p e) e x) (p (set! x (cons x '(2))))) #+END_SRC What are the values of ~(p1 1)~ and ~(p2 1)~ with the original ~eval-sequence~? What would the values be with Cy's proposed change to ~eval-sequence~? c. Cy also points out that changing ~eval-sequence~ as he proposes does not affect the behavior of the example in part a. Explain why this is true. d. How do you think sequences ought to be treated in the lazy evaluator? Do you like Cy's approach, the approach in the text, or some other approach? ** Exercise 4.31 The approach taken in this section is somewhat unpleasant, because it makes an incompatible change to Scheme. It might be nicer to implement lazy evaluation as an "upward-compatible extension", that is, so that ordinary Scheme programs will work as before. We can do this by extending the syntax of procedure declarations to let the user control whether or not arguments are to be delayed. While we're at it, we may as well also give the user the choice between delaying with and without memoization. For example, the definition #+BEGIN_SRC scheme (define (f a (b lazy) c (d lazy-memo)) ...) #+END_SRC would define ~f~ to be a procedure of four arguments, where the first and third arguments are evaluated when the procedure is called, the second argument is delayed, and the fourth argument is both delayed and memoized. Thus, ordinary procedure definitions will produce the same behavior as ordinary Scheme, while adding the ~lazy-memo~ declaration to each parameter of every compound procedure will produce the behavior of the lazy evaluator defined in this section. Design and implement the changes required to produce such an extension to Scheme. You will have to implement new syntax procedures to handle the new syntax for ~define~. You must also arrange for ~eval~ or ~apply~ to determine when arguments are to be delayed, and to force or delay arguments accordingly, and you must arrange for forcing to memoize or not, as appropriate. * <<4.2.3>> Streams as Lazy Lists