core.async Code Style
From OO to Clojure Workshop!
Watch my free workshop to help you learn Clojure faster and shift your paradigm to functional.
Summary: If your functions return
core.async channels instead of
taking callbacks, you encourage them to be called within
Unchecked, this encouragement could proliferate your use of
unnecessarily. There are some coding conventions that can minimize this
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)
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
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
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
(<<< 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
core.async. These practices have taken me a long way.
If you know Clojure and you are interested in learning
core.async in a
fun, interactive style, check out the LispCast Clojure core.async