core.async Code Style
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
block.
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.
Furthermore, 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
core.async
?
<<<
There's a common pattern in Javascript (not ubiquitous, but very common) to put the callback at the end of the parameter list. Since the callback is last, you can easily write something to add it automatically.
(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.
<convention
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)))))
Conclusion
These conventions are a great compromise between ease of using
core.async
(<<<
) 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.
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.