4.2 Variations on a Scheme—Lazy Evaluation
- We can experiment with different language design just by modifying the evaluator.
- This is often how new languages are invented. It’s easy to iterate on a high level evaluator, and it also allows stealing features from the underlying language.
# 4.2.1 Normal Order and Applicative Order
- In § 1.1.5, we noted that Scheme is an applicative-order language.
- Normal-order languages use lazy evaluation to delay evaluation as long as possible.
- Consider this procedure:
(define (try a b) (if = a 0) 1 b)
- In Scheme,
(try 0 (/ 1 0))
causes a division-by-zero error. With lazy evaluation, it does not because the value ofb
is never needed. - In a lazy language, we can implement
if
as an ordinary procedure.
# 4.2.2 An Interpreter with Lazy Evaluation
- In this section, we will modify the interpreter to support lazy evaluation.
- The lazy evaluator delays certain arguments, transforming them into thunks.
- The expression in a thunk does not get evaluated until the thunk is forced.
- A thunk gets forced when its value is needed:
- when passed to a primitive procedure;
- when it is the predicate of conditional;
- when it is an operator about to be applied as a procedure.
- This is similar to streams, but uniform and automatic throughout the language.
- For efficiency, we’ll make our interpreter memoize thunks.
- This is called call-by-need, as opposed to non-memoized call-by-name.
- It raises subtle and confusing issues in the presence of assignments.
Highlights
The word thunk was invented by an informal working group that was discussing the implementation of call-by-name in Algol 60. They observed that most of the analysis of (“thinking about”) the expression could be done at compile time; thus, at run time, the expression would already have been “thunk” about. (Footnote 4.34)
# Modifying the evaluator
- The main change required is the procedure application logic in
eval
andapply
. - The
application?
clause ofeval
becomes:
((application? exp)
(apply (actual-value (operator exp) env)
(operands exp)
env))
- Whenever we need the actual value of an expression, we force in addition to evaluating:
(define (actual-value exp env) (force-it (eval exp env)))
- We change
apply
to takeenv
, and uselist-of-arg-values
andlist-of-delayed-args
:
(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))))
- We also need to change
eval-if
to useactual-value
on the predicate:
(define (eval-if exp env)
(if (true? (actual-value (if-predicate exp) env))
(eval (if-consequent exp) env)
(eval (if-alternative exp) env)))
# Representing thunks
- To force a thunk, we evaluate it in its environment. We use
actual-value
instead ofeval
so that it recursively forces if the result is another thunk:
(define (force-it obj)
(if (thunk? obj)
(actual-value (thunk-exp obj) (thunk-env obj))
obj))
- We can represent thunks simply by a list containing the expression and environment:
(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))
- To memoize thunks,
force-it
becomes a bit more complicated.
# 4.2.3 Streams as Lazy Lists
- Before introducing lazy evaluation, we implemented streams as delayed lists.
- We can instead formulate streams as lazy lists. There are two advantages:
- No need for special forms
delay
andcons-stream
. - No need for separate list and stream operations.
- No need for special forms
- All we need to do is make
cons
lazy, either by introducing non-strict primitives or by definingcons
,car
, andcdr
as compound procedures. - Now we can write code without distinguishing normal lists from infinite ones:
(define ones (cons 1 ones))
(define (scale-list items factor) (map (lambda (x) (* x factor)) items))
- These lazy lists are even lazier than our original streams, since the
car
is delayed too. - This also eliminates the problems we had earlier around having to explicitly delay and force some arguments when computing integrals.