Summary: If your functions return
core.async channels instead of taking callbacks, you encourage them to be called within
go blocks. Unchecked, this encouragement could proliferate your use of
go blocks unnecessarily. There are some coding conventions that can minimize this problem.
I’ve been using (and enjoying!)
core.async, mostly in ClojureScript. It has been a huge help for easily building concurrency patterns that would be incredibly difficult to engineer (and maintain and change) without it.
Over that year, I’ve developed some practices for writing code with
core.async. I’m putting them here as an invitation for discussion.
Use callback style, if possible
A style develops when using
core.async where you convert what would in regular ClojureScript be a callback style with return-a-channel style. The channel will contain the result of the call when it is ready.
Using this style to keep you out of “callback hell” is overkill. “Callback hell” is not caused by a single callback. It is caused by the eternal damnation of coordinating multiple callbacks when they could be called in any order at any time. Callbacks invert control.
core.async quenches the hellfire because coordinating channels within a
go block is easy. The
go block decides which values to read in which order. Control is restored to the code in a procedural style.
But return-a-channel style is not exactly free of sin. If you return a channel too much, the code that calls those functions will likely end up in a
go blocks will proliferate.
go blocks incur extra cost, especially in ClojureScript where they happen asynchronously, meaning at the next iteration of the event loop, which is indeterminately far away.
go blocks might begin nesting (a function whose body is a
go block is called by another function whose body is a
go block, etc), which is correct semantically but probably won’t give you the performance you’re looking for. It’s best to avoid it.
“How?” you say? The most important rule is to only use
core.async in a particular function when necessary. If you can get by with just a callback, don’t use
core.async. Just use a callback. For instance, let’s say you have an
ajax function that takes a callback and you’re trying to make a small API wrapper for convenience. You could make it return a channel like this:
(defn search-google [query] (let [ c (chan)] (ajax (str "http://google.com/?q=" query) #(put! c %)) c))
The interesting thing to note is that
core.async is not being used very well above. Yes, you get rid of a callback, but there isn’t much coordination happening, so it’s not needed. It’s best to keep it straightforward, like this:
(defn search-google [query cb] (ajax (str "http://gooogle.com/?q=" query) cb))
You’re just doing one bit of work here (basically constructing a URL), which is a good sign. But how do you “lift” this into
(defn <<< [f & args] (let [ c (chan)] (apply f (concat args [(fn [x] (if (nil? x) (close! c) (put! c x)))])) c))
This little function is very handy. It automatically adds a callback to a parameter list. You call it like this:
(go (js/console.log (<! (<<< search-google "unicorn droppings"))))
This function lifts
search-google, a regular asynchronous function written with callback style, into
core.async return-a-channel style. With this function, if I always put the callback at the end, I can use my functions from within regular ClojureScript code and also from
core.async code. I can also use any function (and there are many) that happen to have the callback last. This convention has two parts: always put the callback last and use
<<< when you need it. With this function, I can reserve
core.async for coordination (what it’s good at), not merely simple asynchrony.
There are times when writing a function using
go blocks and returning channels is the best way. In those cases, I’ve adopted a naming convention. I put a
< prefix in front of functions that return channels. I tried it at the end of the name, but I like how it looks at the beginning.
(go (js/console.log (<! (<do-something 1 2 3))))
The left-arrow of
<do-something fits right into the
<!. It also visually matches
(<<< do-something 1 2 3), so it makes correct code look correct and wrong code look wrong. The naming convention extends to named values as well:
(def <values (chan)) (go (while true (js/console.log (inc (<! <values)))))
These conventions are a great compromise between ease of using
<<<) and universality (callbacks being universal in JS). The naming convention (
< prefix) visually marks code that should be used with
core.async. These practices have taken me a long way. I’d love to discuss them with you here.
If you know Clojure and you are interested in learning
core.async in a fun, interactive style, check out the LispCast Clojure core.async videos.