mirror of
https://github.com/correl/sicp.git
synced 2024-11-23 11:09:57 +00:00
434 lines
14 KiB
Org Mode
434 lines
14 KiB
Org Mode
#+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:
|
|
<RESPONSE>
|
|
#+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:
|
|
<RESPONSE>
|
|
#+END_SRC
|
|
- RESPONSE:: ~10~
|
|
|
|
The ~id~ of ~10~ is ~10~.
|
|
-
|
|
#+BEGIN_SRC scheme
|
|
;;; L-Eval input:
|
|
count
|
|
;;; L-Eval value:
|
|
<RESPONSE>
|
|
#+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:
|
|
<RESPONSE>
|
|
#+END_SRC
|
|
|
|
-
|
|
#+BEGIN_SRC scheme
|
|
;;; L-Eval input:
|
|
count
|
|
;;; L-Eval value:
|
|
<RESPONSE>
|
|
#+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
|