Skip to content

jessesherlock/ergo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ergo

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

Install

com.jessesherlock/ergo {:mvn/version "0.2.0"} / [com.jessesherlock/ergo "0.2.0"]

Iterate

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]

Stopping iteration

Since (take n) to stop iteration isn’t viable for most use cases, there are a number of useful utility functions in ergo.core to stop iteration, on nil, on an arbitrary predicate fn, when the sequence reaches a fixed point or on an error. There are also utility functions for building more complex iterated sequence transducers.

Exceptions

ergo.core also has utility functions to handle exceptions being caught and put in the transduction like a normal value. Useful for wanting to halt on errors or recovering from exceptions

Async

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.

A normal ergo/iterate and the clojure.core.async version of transduce for when your input is async, and the ergo.async namespace when the iterated function takes a normal value and returns a core.async channel.

(require '[clojure.core.async :as a])
(require '[ergo.async])

(defn async-inc [x] (a/go (inc x))) ; our example channel-returning function

(a/<!! (transduce (comp (ergo.async/iterate async-inc) (take 5)) conj [] [(a/to-chan! [1])]))
; [1 2 3 4 5]

If using transduce 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 where the seed value may be in a channel or may not.

(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, useful for reducing/transducing values onto a core async channel.

(let [x (reduce ergo.async/put-rf! (a/chan 10) [1 2 3 4 5 nil])]
  (a/<!! (a/into [] x)))
; [1 2 3 4 5]

Async Utils

When working with a pipeline of single values in an async context, especially if you process the collection of iteration steps, promise-channels become essential, if the result of an iteration step is a normal core async channel with a single value on it, then the next step of the iteration will take! that value and your iteration result will be a series of empty channels. Promise channels are the ideal solution to a series of single value channels that maybe read multiple times.

ergo.async-utils has a number of functions for converting to promise-channels, as well as exception handling. It also has promise-channel returning and exception catching versions of clojure.core.async/go and exception rethrowing versions of <!~/~<!!, <?~/~<??.

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 versions of <! and <? for mixed contexts that accept an argument that may or may not be a core.async channel, ?<! and ?<?.

Maintenance status

This library is being used for real world stuff. 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 2025-05-24 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.

Contributing

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

About

Clojure library for transducer based Iteration

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors