Skip to content

carlosganzerla/cl-gen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 

Repository files navigation

Common Lisp Generators

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.

Idea

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.

Usage

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 *)
NIL

When 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.

Iteration

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 *)
...
2

We 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 
NIL

Another 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 
NIL

yield 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.

Constraints

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.

Next steps

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.

Releases

No releases published

Packages

No packages published