15
<!-- Topic: Lazy Evaluator
Reading: Abelson, Sussman, Section 4.2, 4.3
Lazy Evaluator
To load the lazy metacircular evaluator, use the following command:
(load "~cs61a/lib/lazy.scm")
Streams and Pairs
Streams require careful handling. The textbook defines the pairs
procedure as follows:
(define (pairs s t)
(cons-stream
(list (stream-car s) (stream-car t))
(interleave
(stream-map (lambda (x) (list (stream-car s) x))
(stream-cdr t))
(pairs (stream-cdr s) (stream-cdr t)))))
In Exercise 3.68, Louis Reasoner suggests a simpler version:
(define (pairs s t)
(interleave
(stream-map (lambda (x) (list (stream-car s) x)) t)
(pairs (stream-cdr s) (stream-cdr t))))
However, this version fails because interleave
evaluates its arguments immediately, leading to infinite recursion. The textbook's version uses cons-stream
, which defers evaluation of its second argument until it is needed.
Explicit Delaying
To make Louis's version work, we can use explicit delaying:
(define (interleave-delayed s1 delayed-s2)
(if (stream-null? s1)
(force delayed-s2)
(cons-stream
(stream-car s1)
(interleave-delayed (force delayed-s2)
(delay (stream-cdr s1))))))
(define (pairs s t)
(interleave-delayed
(stream-map (lambda (x) (list (stream-car s) x)) t)
(delay (pairs (stream-cdr s) (stream-cdr t)))))
While this works, it requires careful handling of delays, which defeats the purpose of abstraction.
Automatic Delay
Lazy evaluation delays all arguments automatically. This means Louis's pairs
procedure would work without explicit delays if every argument were automatically delayed.
To implement this, we modify the evaluator:
- Use
actual-value
instead ofeval
to find the procedure. - Use
list-of-delayed-values
instead oflist-of-values
to delay arguments.
In apply
, we force arguments when calling a primitive procedure:
(define (apply procedure arguments)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure procedure (map force arguments)))
...))
Delay and Force Implementation
Instead of using Scheme's built-in delay
, we define delay-it
and force-it
for the metacircular evaluator:
(define (delay-it exp env)
(list 'thunk exp env))
(define (force-it obj)
(if (thunk? obj)
(let ((exp (thunk-exp obj))
(env (thunk-env obj)))
(set-car! obj (actual-value exp env))
(set-car! (cdr obj) 'evaluated)
(car obj))
obj))
Memoization
Memoization avoids redundant computations by storing results of previously evaluated thunks:
(define (delay-it exp env)
(let ((thunk (list 'thunk exp env)))
(lambda ()
(if (eq? (car thunk) 'thunk)
(let ((result (actual-value (cadr thunk) (caddr thunk))))
(set-car! thunk 'evaluated)
(set-car! (cdr thunk) result)
result)
(cadr thunk)))))
``` -->
#### Nondeterministic Evaluator
To load the nondeterministic metacircular evaluator, use:
```scheme
(load "~cs61a/lib/ambeval.scm")
Solution Spaces and Backtracking
Many problems involve finding solutions that satisfy certain conditions. The solution space can be represented as a stream, and stream-filter
can be used to select valid solutions.
Amb and Require
The amb
special form represents multiple possible values. The require
procedure causes a failure if a condition is not met:
(define (require condition)
(if (not condition) (amb)))
Recursive Amb
amb
can be used recursively to generate infinite solution spaces:
(define (an-integer-between from to)
(if (> from to)
(amb)
(amb from (an-integer-between (+ from 1) to))))
Continuations
Continuations allow computations to be paused and resumed. In the nondeterministic evaluator, eval
is given two continuations: one for success and one for failure.
Implementing Continuations
In eval-if
, we handle continuations as follows:
(define (eval-if exp env succeed fail)
(eval (if-predicate exp)
env
(lambda (pred-value fail2)
(if (true? pred-value)
(eval (if-consequent exp) env succeed fail2)
(eval (if-alternative exp) env succeed fail2)))
fail))
For eval-amb
, we pass a new failure continuation:
(define (eval-amb exp env succeed fail)
(if (null? (cdr exp))
(fail)
(eval (cadr exp)
env
succeed
(lambda ()
(eval-amb (cons 'amb (cddr exp))
env
succeed
fail)))))
Mutation and Continuations
Mutation (e.g., set!
) must handle continuations to ensure correct behavior when computations fail.
Note: The second part of Programming Project 4 is due this week.
This revised note should be clearer and more concise, making it easier to understand the concepts of lazy evaluation and the nondeterministic evaluator.