The cl-gen project is a lib that intends to implement generators similar to
Javascript
generators
in CL. To do that, a small lib of
continuation
macros based on Paul Graham's On Lisp
macros presented on chapter 20, plus a few tweaks and fixes.
Generators are special in that they may execute a block of code lazily, and
pause the execution of the block after calling yield. The generator may be
called again and the code execution will resume from the previous point.
Generators are also very useful for creating lazy sequences (like
IEnumerable/Seq in .NET). In Common Lisp, since there's no (AFAIK) built-in
function to access the whole call stack, implementing generators is rather
tricky. Continuations make this possible. Since they are not built-in in CL as
they are in Scheme, I created a small lib based on PG's book, updated to fix
some stuff that may cause trouble. The generator macros were based on the
continuation macros to allow pause/resuming of execution and provide a similar
experience to JS.
It's not essential to understand continuations to use this lib. Since their
implementation is only a means to an end, I'll let the source document itself.
There's this
article
that walks through the macros presented on PG's book. A particular difference
is that the continuations implemented here use exclusively a well defined
lexical context, so every code that uses continuation must be called within a
cc-context macro. Just put it on the main function and the macros will do the
rest.
On generators we have two core macros: yield-bind and next-bind. They are
parallel to yield and next on JS respectively. Using continuations we may
also establish the bindings for the values that yield and next would
receive. yield-bind basically evaluates and yields the result of a form. It
returns a generator structure that contains the next function call (the
body after the yield). The defgen is a defuncc (defun with
continuations enabled) with a top yield-bind to
simulate the lazy behavior of function*. For example if we define the
following generator and call it:
(defgen generator ()
(yield-bind () "Lorem"
(yield-bind () "Ipsum"
(yield-bind () "Dolor"))))EXAMPLES> (cc-context (generator))
#S(CL-GEN::GENERATOR
:CALL #<FUNCTION (LAMBDA (&OPTIONAL &REST #:G0) :IN %GENERATOR) {5361A13B}>)
NIL
CL-USER>We can see that two values were returned. The first value is always the
generator structure. The other values are the rest of the values returned from
the function body. Suppose we cheat and call the internal function %next
directly:
EXAMPLES> (cl-gen::%next *)
#S(CL-GEN::GENERATOR
:CALL #<FUNCTION (LAMBDA (&OPTIONAL &REST #:G2)
:IN
EXAMPLES::%GENERATOR) {5361A19B}>)
"Lorem"
EXAMPLES>The first yielded value was returned (just as we've called the first next) on
JS. The first value again is a generator structure, but this time with the call
corresponding to the next yield-bind. Generator structures are immutable and
do not exhaust like in JS.
The * means the last returned value on my REPL (I use SLIMV).
If we repeat the process thrice:
EXAMPLES> (cl-gen::%next *)
#S(CL-GEN::GENERATOR
:CALL #<FUNCTION (LAMBDA (&OPTIONAL &REST #:G4) :IN %GENERATOR) {5361A1FB}>)
"Ipsum"
EXAMPLES> (cl-gen::%next *)
#S(CL-GEN::GENERATOR
:CALL #<FUNCTION (LAMBDA (&OPTIONAL &REST #:G6) :IN %GENERATOR) {5361A25B}>)
"Dolor"
EXAMPLES> (cl-gen::%next *)
NILWhen there are no more calls a single NIL is returned.
We can see that we have the equivalent as this:
function* generator() {
yield "Lorem";
yield "Ipsum";
yield "Amet";
}
const gen = generator();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());And the result is somewhat similar:
{ value: 'Lorem', done: false }
{ value: 'Ipsum', done: false }
{ value: 'Amet', done: false }
{ value: undefined, done: true }Of course %next is not exported, so you should not use it. To consume a
generator, we use the next-bind macro:
(cc-context
(let ((gen (generator)))
(next-bind (x) (gen)
(print x)
(next-bind (y) (gen)
(print y)
(next-bind (z) (gen)
(print z))))))"Lorem"
"Ipsum"
"Dolor"
"Dolor"
EXAMPLES> The last "Dolor" was returned because the last form is returned, just like
any CL code block. More simple usage examples are on the examples
file.
Commonly, generators are used on iteration and sequence generation. For a more
realistic and common usage, some utility macros were designed:
generator-bind, generator-loop and do-yield.
do-yield has the same semantics of do. The difference is that the body is
yielded. Take a sequence generator for instance:
(defgen generate-seq (&key (init 0) (end nil) (step 1))
(do-yield ((x init (+ x step)))
((and end (>= x end)))
x))Consuming it we have
SEQUENCES> (cc-context (generate-seq))
(%next cl-gen::%next *)
...
0
(%next cl-gen::%next *)
...
1
(%next cl-gen::%next *)
...
2We can consume the generator automatically using generator-loop, which will
iterate a generator until exhaustion:
(cc-context
(generator-loop (x) (generate-seq :end 5)
(print x)))0
1
2
3
4
NILAnother macro useful for iteration generator-bind. The user may decide wether
to call the next iteration or skip it.
(defgen even-seq (seq)
(generator-bind (x) seq
(if (evenp x)
(yield x)
(next))))
(cc-context
(generator-loop (x) (even-seq (generate-seq :end 10))
(print x)))2
4
6
8
NILyield and next are local functions only valid on the context established by
generator-bind. Next skips the iteration, while yield yields the value and
will resume on the next iteration after the generator is called again. yield
is a tail recursive call on the context of generator-bind, so it should be
called as a tail recursive function would to avoid weird behavior or stack
overflows.
A caveat is that these iteration macros are built on top of tail-recursive functions. See the constraints section for more details.
There's a good example of usage of these macros and generator combination on the sequences example file.
The lib provides similar experince to JS, with some constraints. The first being that you must use a continuation context to use them. Using it on the top-level should suffice but the overhead that it may cause to larger programs it is yet to be discovered.
The second constraint is that generator forwarding or consumption must be tail recursive in general. For a small number of possible iterations it won't really matter, but this scenario isn't really applicable to generators that are used to simulate infinite sequences.
The third is the absence of some additional capabilities of JS generators as unwindings and throwing exceptions from the outside. I didn't try to implement these, but they may be feasible. The current objective is to implement a minimal and functional interface with an easy (or at least not very hard) to use API.
Previously, this lib implementation didn't have the pause functionality. It
provided a nice API to decouple functions, but didn't introduce nothing
relevant and all it did was some boilerplate reduction since everything it did
could be done by simple functional programming without much fuss (see the
old branch).
I resisted to use continuations for some time but basically
the functionality can't be reproduced without it AFAIK.
The next step IMO would be to improve the API. The direcitons for it depends really on the use cases. The core macros may be paired with recursive functions that don't fit in the existing iteration macros and new abstractions may arise from this. I'll be trying to use this lib for other stuff and adding new features along the way should they be generic enough.
I'll add tests and docstrings soon.
Feel free to open PRs, issues or contact me.