Implementation of lseq-for-each
Wolfgang Corcoran-Mathe 11 Feb 2023 18:11 UTC
Hi,
The sample implementation of lseq-for-each has some surprising
behavior. Here’s the implementation:
(define (lseq-for-each proc . lseqs)
(apply for-each proc (map lseq-realize lseqs)))
Rather than traversing the lseqs and applying proc to their cars
at each step, it fully forces them before traversing. There are
two problems here.
1. Fully forcing the lseqs wastes space. A major reason to use
lseqs is to avoid building huge lists. Thus, ignoring proc’s needs,
lseq-for-each should operate in constant space.
2. Effects occur in an unexpected order. This is more interesting.
Forcing the cdr of an lseq can have side effects, as can calling proc;
indeed, proc is called for its side effects. When do these different
effects occur? Consider this common kind of I/O stream:
(define chars (generator->lseq read-char))
Calling ‘lseq-cdr’ on chars causes a side effect: reading a character.
Now, combine this with an output procedure:
(lseq-for-each write-char chars)
The read and write effects could occur in (at least) two different
orders:
read
write
read
write
...
or
read
read
...
read
write
write
...
write
The SRFI doesn’t tell us which to expect, but the latter, “clustered”
pattern is bad. It will sponge up all available input before producing
any output, hanging the program if ‘read-char’ blocks. “Interleaving”
the effects, as in the first pattern, avoids these problems.
***
The fix for both problems is simple: just implement ‘lseq-for-each’
directly. I’ll send a pull request with this implementation shortly.
I think a note about effect order should also be added to the
‘lseq-for-each’ specification. Perhaps this: “If forcing the cdr of
lseq entails side effects, then those effects are interleaved with
the effects of calling proc.”
Regards,
Wolf
--
Wolfgang Corcoran-Mathe <xxxxxx@sigwinch.xyz>