ergo [ur-goh, er-goh]; adverb; Consequently, therefore, hence.
Ergo is a small Clojure/Clojurescript library for iteration in a tranducer style, for iterating both regular functions and functions returning a core.async channel (for now, manifold coming soon).
com.jessesherlock/ergo {:mvn/version "0.1.3-alpha"}
/ [com.jessesherlock/ergo "0.1.3-alpha"]
Iterate takes a function f and a seed value x and returns an infinite list of x, (f x), (f (f x)), (f (f (f x))), etc The clojure core version returns a lazy list, but I find it very useful to use an eager version that can be used with other transducers, hence Ergo.
(require '[ergo.core :as ergo])
;; Exactly the clojure core lazy iterate if given two arguments
(ergo/iterate inc 1)
; (1 2 3 4 5 ....)
;; Used like a tranducer with 1 argument
(transduce (comp (ergo/iterate inc)
(take 5))
conj
[]
[1])
; [1 2 3 4 5]
Using iterate as a transducer means it will take the first value in the collection being and use that as a seed, never touching any other values. Since iteration isn’t a type of reduction, it’s more of a sibling of the reducing concept (map is list->list, reduce is list->value, iterate is value->list), but it is still very useful to use it like this.
Ergo has a produce
fn that works like transduce but takes a value instead of a collection so that you don’t have to wrap the seed in a collection all the time for no reason.
(ergo/produce (comp (ergo/iterate (partial * 2)) (take 5)) conj [] 1)
; [1 2 4 8 16]
Since (take n) to stop iteration isn’t viable for many/most use cases, there are a number of useful utility functions in ergo.core
to stop on a nil value, when values reach a fixed point, until an arbitrary predicate returns true, until an exception is thrown and some others, as well as utility functions to build your own versions.
Look at the tests for examples of all of them.
Like any transducer you can use iterate in an async transducing context (where the seed is in a core async channel)
(require '[clojure.core.async :as a])
(let [ch (a/chan)
_ (a/onto-chan! ch [1])]
(a/<!! (a/transduce (comp (ergo/iterate inc) (take 5)) conj [] ch)))
; [1 2 3 4 5]
But when using iterate for data pipelines/interceptors/state machines you will come across situations where your seed isn’t in a channel but your transition function, the one you want to iterate, returns a core.async channel.
This is what the ergo.async
namespace is for.
(require '[clojure.core.async :as a])
(require '[ergo.async :as ea])
(defn async-inc [x] (a/go (inc x))) ; our example channel returning transition function
(a/<!! (transduce (comp (ea/iterate async-inc) (take 5)) conj [] [(a/to-chan! [1])]))
; [1 2 3 4 5]
ergo.async/iterate
expects the seed to be in a channel (since it will be in a channel for every iteration but the first due to your async transition function)
It also has a useful version of produce
(a/<!! (ea/produce (comp (ea/iterate async-inc) (take 5)) conj [] 1))
; [1 2 3 4 5]
and a function called put-rf!
which works like core.async/put!
but can be used as a reducer, very useful for reducing/transducing values onto a core async channel.
When working with a pipeline of single values in an async context, especially if you process the collection of iteration steps then promise-channels are your friend, since normal mutable channels require a lot of care, ergo.async-utils
has a number of functions for coercion to promise-channels as well as exception handling.
My main use case for this library is for state machines with transitions functions that may or may not return an async value, depending on the state, so there are also a number of utility functions for working with mixed values or coercing multiple types into a promise-channel.
This library is being used for real world stuff, and is only marked alpha because I still have a bunch of projects that I will use it in, so it’s not impossible for API changes to happen. I consider it very unlikely, it is supported and will continue to be supported for the foreseeable future. Please file bugs, use it if you need it, and expect me to maintain this library. This is true as of 2024-09-01 and I will update that date semi regularly so you know this isn’t abandoned, the goal is to finish this library and make no more changes and these days people often read that as being abandoned, hence this message.
TODO - the tests have good comments and should cover everything, and the library isn’t that large anyway but I should write up a walkthrough of everything at some point.
Running the clj tests
clj -A:test -M -m koacha.runner
clj -A:test -M -m koacha.runner --watch
Running the cljs tests
clj -A:shadow watch test
Then open the listed webpage (the one after “HTTP server available at”) to see the test results
Fire up an nrepl server with rebel-readline, the tests, shadow and criterium in the classpath
clj -A:repl