Is core.async Against the Clojure Philosophy?
Summary: Clojure core.async is a way to manage mutable state. Isn't that against functional programming?
When core.async was first announced, there was a lot of fanfare. But
among the celebration, there was some consternation about core.async.
Isn't core.async against the functional principles of Clojure?
Aren't channels just mutable state? Aren't the <!
and >!
operations mutation?
Well, it's true. core.async is about mutation. It's procedural code. Go blocks run their bodies one step at a time. It's imperative.
But that's what Clojure is all about. It makes functional programming
easy (with fn
s, immutable data
structures,
and higher order functions). It also makes mutable state easy to reason
about. It does not eliminate it. It simply gives you better
abstractions. That's what
Atoms,
Refs,
Vars, and
Agents are: useful abstractions for dealing with state.
core.async is just another abstraction for dealing with state. But, following the Clojure philosophy, it was chosen to be easy to reason about. The hardest part about coordinating and communicating with independent threads normally is that neither of them know what the other is doing. You can make little signals using shared memory. But those signals get complicated fast once you scale past two threads.
And that's what a channel is: it's just a shared coordination point. But it has some cool properties that make it super easy to reason about:
- Carry semantics: the channel carries its own coordination semantics (buffering, unbuffered, etc).
- Simple interface: channels have put, take, and close. That's it.
- Very scalable: any number of processes can use a single channel with no additional cost.
- Decoupling: consumers don't need to know producers and vice versa.
Channels are awesome, but they're not the whole story. The other part of core.async is the go block. Go blocks are another abstraction. They allow you to write code in an imperative style that blocks on channels. You get to use loops and conditionals, as well as local let variables, global variables, and function calls — everything you're already using, but augmented with the coordination power of channels.
All of these features add up to something you can reason about locally. That's the key: the code you're looking at now can be understood without looking at other code.
But there's a downside: you now have more choices. In theory, they're easier choices. But that requires you to understand the choices. You need to understand the abstractions, the idioms, and the tradeoffs. That's the goal of the LispCast Clojure core.async video course. If you'd like to use core.async but you don't know where to start, this is a good place.