core.async Code Style

Eric Normand's Newsletter
Software design, functional programming, and software engineering practices
Over 5,000 subscribers

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.

Sean Allen
Sean Allen
Your friendly reminder that if you aren't reading Eric's newsletter, you are missing out…
👍 ❤️
Nicolas Hery
Nicolas Hery
Lots of great content in the latest newsletter! Really glad I subscribed. Thanks, Eric, for your work.
👍 ❤️
Mathieu Gagnon
Mathieu Gagnon
Eric's newsletter is so simply great. Love it!
👍 ❤️