/generator

Primary LanguageClojureEclipse Public License 2.0EPL-2.0

  • CircleCI
  • Clojars Project

Generator

The generator library brings js-like generators, also known as continuations, to Clojure(Script).

Generators are useful for building effect systems like:

  • redux-saga for JavaScript. This is an awesome example of using generators. Check it out first!
  • darkleaf/effect for Clojure(Script)

Special thanks to @leonoel for his cloroutine.

(require '[darkleaf.generator.core :as gen :refer [generator yield]])

(let [f*  (fn []
            (generator
             (yield :my-value)))
      gen (f*)]
  (assert (= :my-value (gen/value gen)))
  (gen/next gen :my-covalue)
  (assert (= :my-covalue (gen/value gen)))
  (assert (gen/done? gen)))

For more examples, see the test suite.

Continuations are not first class citizens in an underluing platform like JVM or V8, so we face with colored functions. Functions that return a generator are red in this terminology, and regular functions are blue. We can't pass our red functions to blue ones. For example we can't pass them to functions like map or reduce. So the library provides gen/mapv* and gen/reduce*.

By default generators are stackless, so if you want to call one red function from another one, you have to use gen/wrap-stack middleware:

(let [nested* (fn []
                (generator
                 [(yield :a)
                  (yield :b)]))
      f*      (fn []
                (generator
                 [(yield :start)
                  (yield (nested*))
                  (yield :finish)]))
      f*      (gen/wrap-stack f*)
      gen     (f*)]
  (assert (= :start (gen/value gen)))
  (gen/next gen 1)
  (assert (= :a (gen/value gen)))
  (gen/next gen 2)
  (assert (= :b (gen/value gen)))
  (gen/next gen 3)
  (assert (= :finish (gen/value gen)))
  (gen/next gen 4)
  (assert (= [1 [2 3] 4] (gen/value gen)))
  (assert (gen/done? gen)))

Fortunately, there is Project Loom, which will bring first-class continuations on the JVM. With Loom, it is possible to use yield (1) in regular nested functions called by generator (3). Also, they can be passed into regular higher-order functions like mapv (2):

(ns darkleaf.generator.loom-test
  (:require
   [darkleaf.generator.core :as gen]
   ;; Loom support is in a separate namespace
   [darkleaf.generator.loom :refer [generator yield]]
   ...))

(t/deftest loom-killer-feature-test
  (let [nested (fn [x]
                 (yield [:inc x]))      ;; (1)
        f      (fn []
                 (mapv nested [0 1 2])) ;; (2)
        f*     (fn []
                 (generator             ;; (3)
                  (f)))
        gen    (f*)]
    (t/is (= [:inc 0] (gen/value gen)))
    (gen/next gen 1)
    (t/is (= [:inc 1] (gen/value gen)))
    (gen/next gen 2)
    (t/is (= [:inc 2] (gen/value gen)))
    (gen/next gen 3)

    (t/is (= [1 2 3] (gen/value gen)))
    (t/is (gen/done? gen))))

To play with it you need to use Loom's early access builds and an special version of this library. Check out tests.

Pro tips

You can use threading macors like this:

(generator
 (-> value
     regular-fn
     (-> gen-fn* yield)
     other-regular-fn
     (-> other-gen-fn* yield)))