racket/doc/srfi-std/srfi-45.html
Matthew Flatt 28a3f3f0e7 r5rs and srfi docs and bindings
svn: r9336
2008-04-16 20:52:39 +00:00

786 lines
25 KiB
HTML
Raw Blame History

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>SRFI 45: Primitives for expressing iterative lazy algorithms</title>
</head>
<body>
<H1>Title</H1>
SRFI 45: Primitives for Expressing Iterative Lazy Algorithms
<H1>Author</H1>
Andr&eacute; van Tonder
<H1>Status</H1>
This SRFI is currently in ``final'' status. To see an explanation
of each status that a SRFI can hold, see <A
HREF="http://srfi.schemers.org/srfi-process.html">here</A>. You
can access previous messages via <a
href="http://srfi.schemers.org/srfi-45/mail-archive/maillist.html">
the archive of the mailing list</a>.
<p>
<ul>
<li>Received: 2003/09/20</li>
<li>Draft: 2003/09/23-2003/12/23</li>
<li>Revised: 2003/12/20</li>
<li>Revised: 2004/03/06</li>
<li>Final: 2004/04/05</li>
<li>Bug fix: 2004/08/04</li>
</ul>
<H1>Abstract</H1>
Lazy evaluation is traditionally simulated in Scheme using
<code>delay</code> and <code>force</code>. However, these
primitives are not powerful enough to express
a large class of lazy algorithms that are iterative.
Indeed, it is folklore in the Scheme community that
typical iterative lazy algorithms written using
<code>delay</code> and
<code>force</code> will often
require unbounded memory.
<p>
Although varous modifications of <code>delay</code> and
<code>force</code> had been proposed to resolve this problem (see e.g., the
<a href=
http://srfi.schemers.org/srfi-40/mail-archive/maillist.html>
SRFI-40 discussion list
</a>)
they all fail some of the benchmarks provided below. To our knowledge,
the current SRFI provides the first exhaustive solution to this problem.
<p>
As motivation,
we first explain how the usual laziness encoding using only <code>delay</code>
and <code>force</code> will break the iterative behavior of typical
algorithms that would have been properly tail-recursive
in a true lazy language, causing the computation to require unbounded memory.
<p>
The problem is then resolved by
introducing a set of three operations:
<pre>
{<code>lazy</code>, <code>delay</code>, <code>force</code>}
</pre>
which allow the programmer to succinctly express lazy algorithms while
retaining bounded space behavior in cases that are properly tail-recursive.
A general
recipe for using these primitives is provided. An additional procedure
<code>{eager}</code> is provided for the construction of
eager promises in cases where efficiency is a concern.
<p>
Although this SRFI redefines <code>delay</code> and <code>force</code>,
the extension is conservative in the sense that the semantics of the subset {<code>delay</code>, <code>force</code>} in
isolation (i.e., as long as the program does not use <code>lazy</code>)
agrees with that in R5RS. In other words, no program that uses the
R5RS definitions of delay and force will break if those definition are
replaced by the SRFI-45 definitions of delay and force.
<H1>Rationale</H1>
Wadler et al. in the paper
<em>How to add laziness to a strict language without even being odd</em> [Wad98], provide a
straightforward recipe for transforming arbitrary lazy data structures and algorithms
into a strict language using <code>delay</code> and <code>force</code>.
<p>
However, it is known (see e.g. the <a href="http://srfi.schemers.org/srfi-40/mail-archive/maillist.html">SRFI-40 discussion list</a>) that this
transformation can lead to programs that suffer from unbounded space
consumption, even if the original lazy algorithm was properly tail-recursive.
<H2>Example</H2>
Consider the following procedure, written in a hypothetical lazy
language with Scheme syntax:
<pre>
(define (stream-filter p? s)
(if (null? s) '()
(let ((h (car s))
(t (cdr s)))
(if (p? h)
(cons h (stream-filter p? t))
(stream-filter p? t)))))
</pre>
According to the tranformation proposed in [Wad98], this algorithm can be espressed as follows
in Scheme:
<pre>
(define (stream-filter p? s)
(delay (force
(if (null? (force s)) (delay '())
(let ((h (car (force s)))
(t (cdr (force s))))
(if (p? h)
(delay (cons h (stream-filter p? t)))
(stream-filter p? t)))))))
</pre>
The recipe, <em>which we will modify below</em>, is as follows:
<ul>
<li>
wrap all constructors (e.g., <code>'()</code>, <code>cons</code>) with <code>delay</code>,
</li>
<li>
apply <code>force</code> to arguments of deconstructors (e.g., <code>car</code>,
<code>cdr</code> and <code>null?</code>),
</li>
<li>
wrap procedure bodies with <code>(delay (force ...))</code>.
</li>
</ul>
<p>
However, evaluating the following with a sufficiently value for
<code>large-number</code>
will cause a typical Scheme
implementation to run out of memory, despite the fact that
the original (lazy) algorithm was iterative, only needing tail calls to evaluate the
first element of the result stream.
<pre>
(define (from n)
(delay (cons n (from (+ n 1)))))
(define large-number 1000000000)
(car (force (stream-filter (lambda (n) (= n large-number))
(from 0))))
</pre>
<h2>Why the space leak occurs</h2>
The problem occurring in the above
<code>stream-filter</code> example can already
be seen in the following simple infinite loop, expressed in our hypothetical lazy
language as:
<pre>
(define (loop) (loop))
</pre>
which becomes, according to the [Wad98] transformation
<pre>
(define (loop) (delay (force (loop))))
</pre>
Taking the semantics of {<code>delay</code>, <code>force</code>}
to be informally:
<pre>
(force (delay expr)) = update promise : (delay expr)
with value of expr
return value in promise
</pre>
we get
<pre>
(force (loop)) = update promise1 : (delay (force (loop)))
with value of (force (loop))
return value in promise1
= update promise1 : (delay (force (loop)))
with value of
update promise2 : (delay (force (loop)))
with value of (force (loop))
return value in promise2
return value in promise1
= update promise1 : (delay (force (loop)))
with value of
update promise2 : (delay (force (loop)))
with value of
update promise3 : (delay (force (loop)))
with value of (force (loop))
return value in promise3
return value in promise2
return value in promise1
= ...
</pre>
We see that an ever growing sequence of pending promises builds up until the heap
is exhausted.
<h2>Why the above is not call-by-need</h2>
Expressing the above algorithm in terms of {<code>delay</code>, <code>force</code>}
in fact does not correctly capture common notions of
call-by-need evaluation semantics. For example,
in a call-by-need language with naive graph reduction semantics, the above algorithm
would run in bounded space since naive graph reduction is known to be
tail-safe. For a good discussion of this issue, see e.g. R. Jones -
<em>Tail recursion without space leaks</em> [Jon98].
<p>
Our problem may be regarded as analogous to graph reduction,
with promises corresponding to graph nodes and
<code>force</code> corresponding to reduction. As described by Jones,
one has to be careful with
the order in which nodes are evaluated and overwritten to avoid space
leaks. In our context this would correspond to the order in which
promises are evaluated and overwritten when forced.
<p>
In the above example, naive graph reduction would correspond to the
promise at the root being overwritten at each step <em>before</em>
the next iteration is evaluated, thus avoiding the need for a growing
sequence of unfulfilled promises representing (unnecessary) future
copy operations.
<h2>The solution</h2>
The accumulation of unnecessary promises in the above examples is a consequence
of suspensions being forced in increasingly nested contexts. In order
to correctly simulate naive graph reduction
we should instead find a way of forcing tail suspensions <em>iteratively</em>,
each time overwriting the previous result.
<p>
A solution to this problem exists and is described (in a different context) in
<em>Compiling higher order languages into fully tail-recursive portable C</em>
- Feely et al. [Fee97]. This reference introduces a method
widely known as the <em>trampoline technique</em> for evaluating
tail contexts iteratively.
<p>
Adapting the trampoline technique to the situation at hand, we
introduce a new primitive <code>lazy</code>, which behaves like
an "atomic" <code>(delay (force ...))</code>, and which will
replace the combination <code>(delay (force ...))</code> at procedure
entry points. We also
redefine <code>delay</code> and <code>force</code> as below:
<pre>
; type Promise a = lazy (Promise a) | eager a
(define-syntax lazy
(syntax-rules ()
((lazy exp)
(box (cons 'lazy (lambda () exp))))))
(define (eager x)
(box (cons 'eager x)))
(define-syntax delay
(syntax-rules ()
((delay exp) (lazy (eager exp)))))
(define (force promise)
(let ((content (unbox promise)))
(case (car content)
((eager) (cdr content))
((lazy) (let* ((promise* ((cdr content)))
(content (unbox promise))) ; *
(if (not (eqv? (car content) 'eager)) ; *
(begin (set-car! content (car (unbox promise*)))
(set-cdr! content (cdr (unbox promise*)))
(set-box! promise* content)))
(force promise))))))
(*) These two lines re-fetch and check the original promise in case
the first line of the let* caused it to be forced. For an example
where this happens, see reentrancy test 3 below.
(define (box x) (list x))
(define unbox car)
(define set-box! set-car!)
</pre>
Our example is then coded (see the full recipe below)
<pre>
(define (loop) (lazy (loop)))
</pre>
When we now evaluate <code>(force (loop))</code>,
the <code>force</code> procedure will execute a top-level loop
which will iteratively evaluate and overwrite subsequent
suspensions.
<p>
In the language of [Fee97],
the iterative loop in <code>force</code> plays the role of
"dispatcher".
The <code>lazy</code> form marks "control points" (procedure entry and
return points).
This technique is tail-safe because lazy procedures, instead of calling
other lazy procedures directly, simply return a
suspension representing a control point to be called upon the next iteration
of the dispatcher loop in <code>force</code>. For more details, see [FMRW].
<H1>Specification</H1>
The following macros should be provided. The semantics, which is informally
described here, should conform to that of the reference implementation below:
<ul>
<li>
<code><a name="delay">(delay expression)</a></code>:
Takes an expression of arbitrary type a and returns a promise of type (Promise a)
which at some point in the future may be asked (by the force procedure)
to evaluate the expression and deliver the resulting value.
<li>
<code><a name="lazy">(lazy expression)</a></code>:
Takes an expression of type (Promise a) and returns a promise of type (Promise a)
which at some point in the future may be asked (by the force procedure)
to evaluate the expression and deliver the resulting promise.
</ul>
The following procedures should be provided:
<ul>
<li>
<code><a name="force">(force expression)</a></code>:
Takes an argument of type (Promise a) and returns a value of type a as follows:
If a value of type a has been computed for the promise, this value is returned.
Otherwise, the promise is first evaluated, then overwritten by the obtained promise
or value,
and then <code>force</code> is again applied (iteratively) to the promise.
<li>
<code><a name="eager">(eager expression)</a></code>:
Takes an argument of type a and returns a value of type Promise a. As opposed
to <code>delay</code>, the argument is evaluated eagerly. Semantically,
writing <code>(eager expression)</code> is equivalent to writing
<pre>
(let ((value expression)) (delay value)).
</pre>
However, the former is more efficient since it does not require unnecessary
creation and evaluation of thunks. We also have the equivalence
<pre>
(delay expression) = (lazy (eager expression))
</pre>
</ul>
The following reduction rules may be helpful for reasoning about
these primitives. However, they do not express the memoization and
memory usage
semantics specified above:
<pre>
(force (delay expression)) -> expression
(force (lazy expression)) -> (force expression)
(force (eager value)) -> value
</pre>
<p>
The typing can be succinctly expressed as follows:
<pre>
type Promise a = lazy (Promise a) | eager a
expression : a
------------------------------
(eager expression) : Promise a
expression : Promise a
------------------------------
(lazy expression) : Promise a
expression : a
------------------------------
(delay expression) : Promise a
expression : Promise a
------------------------------
(force expression) : a
</pre>
<p>
Although this SRFI specifies an extension to the semantics of <code>force</code>,
the extension is conservative in the sense that the semantics of the subset {<code>delay</code>, <code>force</code>} in
isolation (i.e., as long as the program does not use <code>lazy</code>)
agrees with that in R5RS.
<H1>Correct usage</H1>
We now provide a general
recipe for using the primitives
<pre>
{<code>lazy</code>, <code>delay</code>, <code>force</code>}
</pre>
to express lazy algorithms in Scheme.
The transformation is best described by way of an example: Consider
again the stream-filter algorithm, expressed in a hypothetical lazy language
as
<pre>
(define (stream-filter p? s)
(if (null? s) '()
(let ((h (car s))
(t (cdr s)))
(if (p? h)
(cons h (stream-filter p? t))
(stream-filter p? t)))))
</pre>
This algorithm can be espressed as follows
in Scheme:
<pre>
(define (stream-filter p? s)
(lazy
(if (null? (force s)) (delay '())
(let ((h (car (force s)))
(t (cdr (force s))))
(if (p? h)
(delay (cons h (stream-filter p? t)))
(stream-filter p? t))))))
</pre>
In other words, we
<ul>
<li>
wrap all constructors (e.g., <code>'(), cons</code>) with <code>delay</code>,
</li>
<li>
apply <code>force</code> to arguments of deconstructors (e.g., <code>car</code>,
<code>cdr</code> and <code>null?</code>),
</li>
<li>
wrap procedure bodies with <code>(lazy ...)</code>.
</li>
</ul>
The only difference with the [Wad98] transformation described above is
in replacing the combination <code>(delay (force ...))</code> with
<code>(lazy ...)</code> in the third rule.
<p>
More examples are included in the reference implementation below.
<H1>Implementation</H1>
The reference implementation uses the macro
mechanism of R5RS.
It does not use any other SRFI or any library.
<p>
A collection of benchmarks is provided.
These check some special cases of the mechanism defined
in this SRFI. To run them the user will need access to some way
of inspecting the runtime memory usage of the algorithms, and
a metric, left unspecified here, for deciding whether the memory
usage is bounded. A leak benchmark is passed if the memory usage
is bounded.
Passing the tests does not mean a correct implementation.
<h2>
Reference implementation
</h2>
<pre>
;=========================================================================
; Boxes
(define (box x) (list x))
(define unbox car)
(define set-box! set-car!)
;=========================================================================
; Primitives for lazy evaluation:
(define-syntax lazy
(syntax-rules ()
((lazy exp)
(box (cons 'lazy (lambda () exp))))))
(define (eager x)
(box (cons 'eager x)))
(define-syntax delay
(syntax-rules ()
((delay exp) (lazy (eager exp)))))
(define (force promise)
(let ((content (unbox promise)))
(case (car content)
((eager) (cdr content))
((lazy) (let* ((promise* ((cdr content)))
(content (unbox promise))) ; *
(if (not (eqv? (car content) 'eager)) ; *
(begin (set-car! content (car (unbox promise*)))
(set-cdr! content (cdr (unbox promise*)))
(set-box! promise* content)))
(force promise))))))
; (*) These two lines re-fetch and check the original promise in case
; the first line of the let* caused it to be forced. For an example
; where this happens, see reentrancy test 3 below.
;=========================================================================
; TESTS AND BENCHMARKS:
;=========================================================================
;=========================================================================
; Memoization test 1:
(define s (delay (begin (display 'hello) 1)))
(force s)
(force s)
;===> Should display 'hello once
;=========================================================================
; Memoization test 2:
(let ((s (delay (begin (display 'bonjour) 2))))
(+ (force s) (force s)))
;===> Should display 'bonjour once
;=========================================================================
; Memoization test 3: (pointed out by Alejandro Forero Cuervo)
(define r (delay (begin (display 'hi) 1)))
(define s (lazy r))
(define t (lazy s))
(force t)
(force r)
;===> Should display 'hi once
;=========================================================================
; Memoization test 4: Stream memoization
(define (stream-drop s index)
(lazy
(if (zero? index)
s
(stream-drop (cdr (force s)) (- index 1)))))
(define (ones)
(delay (begin
(display 'ho)
(cons 1 (ones)))))
(define s (ones))
(car (force (stream-drop s 4)))
(car (force (stream-drop s 4)))
;===> Should display 'ho five times
;=========================================================================
; Reentrancy test 1: from R5RS
(define count 0)
(define p
(delay (begin (set! count (+ count 1))
(if (> count x)
count
(force p)))))
(define x 5)
(force p) ;===> 6
(set! x 10)
(force p) ;===> 6
;=========================================================================
; Reentrancy test 2: from SRFI 40
(define f
(let ((first? #t))
(delay
(if first?
(begin
(set! first? #f)
(force f))
'second))))
(force f) ;===> 'second
;=========================================================================
; Reentrancy test 3: due to John Shutt
(define q
(let ((count 5))
(define (get-count) count)
(define p (delay (if (<= count 0)
count
(begin (set! count (- count 1))
(force p)
(set! count (+ count 2))
count))))
(list get-count p)))
(define get-count (car q))
(define p (cadr q))
(get-count) ; => 5
(force p) ; => 0
(get-count) ; => 10
;=========================================================================
; Test leaks: All the leak tests should run in bounded space.
;=========================================================================
; Leak test 1: Infinite loop in bounded space.
(define (loop) (lazy (loop)))
;(force (loop)) ;==> bounded space
;=========================================================================
; Leak test 2: Pending memos should not accumulate
; in shared structures.
(define s (loop))
;(force s) ;==> bounded space
;=========================================================================
; Leak test 3: Safely traversing infinite stream.
(define (from n)
(delay (cons n (from (+ n 1)))))
(define (traverse s)
(lazy (traverse (cdr (force s)))))
;(force (traverse (from 0))) ;==> bounded space
;=========================================================================
; Leak test 4: Safely traversing infinite stream
; while pointer to head of result exists.
(define s (traverse (from 0)))
;(force s) ;==> bounded space
;=========================================================================
; Convenient list deconstructor used below.
(define-syntax match
(syntax-rules ()
((match exp
(() exp1)
((h . t) exp2))
(let ((lst exp))
(cond ((null? lst) exp1)
((pair? lst) (let ((h (car lst))
(t (cdr lst)))
exp2))
(else 'match-error))))))
;========================================================================
; Leak test 5: Naive stream-filter should run in bounded space.
; Simplest case.
(define (stream-filter p? s)
(lazy (match (force s)
(() (delay '()))
((h . t) (if (p? h)
(delay (cons h (stream-filter p? t)))
(stream-filter p? t))))))
;(force (stream-filter (lambda (n) (= n 10000000000))
; (from 0)))
;==> bounded space
;========================================================================
; Leak test 6: Another long traversal should run in bounded space.
; The stream-ref procedure below does not strictly need to be lazy.
; It is defined lazy for the purpose of testing safe compostion of
; lazy procedures in the times3 benchmark below (previous
; candidate solutions had failed this).
(define (stream-ref s index)
(lazy
(match (force s)
(() 'error)
((h . t) (if (zero? index)
(delay h)
(stream-ref t (- index 1)))))))
; Check that evenness is correctly implemented - should terminate:
(force (stream-ref (stream-filter zero? (from 0))
0)) ;==> 0
(define s (stream-ref (from 0) 100000000))
;(force s) ;==> bounded space
;======================================================================
; Leak test 7: Infamous example from SRFI 40.
(define (times3 n)
(stream-ref (stream-filter
(lambda (x) (zero? (modulo x n)))
(from 0))
3))
(force (times3 7))
;(force (times3 100000000)) ;==> bounded space
</pre>
<H1>References</H1>
[Wad98]
Philip Wadler, Walid Taha, and David MacQueen.
<em>How to add laziness to a strict language, without even being odd</em>,
Workshop on Standard ML, Baltimore, September 1998
<p>
[Jon92]
Richard Jones. <em>Tail recursion without space leaks</em>, Journal of Functional Programming, 2(1):73-79, January 1992
<p>
[Fee97]
Marc Feeley, James S. Miller, Guillermo J. Rozas, Jason A. Wilson,
<em> Compiling Higher-Order Languages into Fully Tail-Recursive Portable C</em>,
Rapport technique 1078, d<>partement d'informatique et r.o., Universit<69> de Montr<74>al, ao<61>t 1997.
<H1>Copyright</H1>
<p>Copyright (C) Andr&eacute; van Tonder (2003). All Rights Reserved.</p>
<p>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
</p>
<p>
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<hr>
<address>Author: <a href="mailto:andre@het.brown.edu">Andr<EFBFBD> van Tonder</a></address>
<address>Editor: <a href="mailto:srfi-editors@srfi.schemers.org">Francisco Solsona</a></address>
<!-- Created: Fri Sep 19 17:55:00 EST 2002 -->
<!-- hhmts start -->
Last modified: Tue Dec 30 11:21:21 CST 2003
<!-- hhmts end -->
</body>
</html>