The more I look at Clojure, the more I think it’s a heroic attempt to Do The Right Thing, in fact All The Right Things, as we move toward the lots-of-not-particularly-fast-cores future. I’m still working my head around Clojure’s concurrency primitives. We come to understand the things we don’t by contrast with the things we do; so I’m finding contrast between the Clojure and Erlang approaches to messaging instructive.

[This is part of the Concur.next series. By way of motivation, consider the recent 100-core processor from Tilera. I quote from the coverage, which says that the new chip should offer “somewhere between seven and eight times the performance of the original Tile64 chip that debuted two years ago. That is well above and beyond the Moore’s Law curve, provided your workload can scale across lots of cores.” Which, absent a good Concur.next solution, most workloads can’t.]

Let’s suppose we’ve got a program we want to smash across lots of cores, and we want it to count things. The counts aren’t going to be reported out till the program finishes, so there’s no reason for the main workload to wait for the counting arithmetic. So the obvious thing to do is to run the counters off in their own thread(s), send them please-count messages (no need to wait for the answers), and the numbers will all get added up eventually.

In Erlang · Here’s how you might do it. Let’s take the simplest possible approach and have a separate process for each item you’re counting.

1 -module(counter). 2 -export([new/0, incr/1, count/1, counter/0]). 3 4 new() -> 5 Counter = spawn(fun counter:counter/0), 6 Counter. 7 8 incr(Counter) -> 9 Counter ! { incr }. 10 11 count(Counter) -> 12 Counter ! { count, self() }, 13 receive 14 { count, Count } -> 15 Count 16 end. 17 18 %- The counter process ------------------- 19 counter() -> 20 counter_loop(0). 21 22 counter_loop(Count) -> 23 receive 24 { incr } -> 25 counter_loop(Count + 1); 26 { count, Requestor } -> 27 Requestor ! { count, Count }, 28 counter_loop(Count) 29 end.

When a user calls the incr function, line 8, that just sends off a one-element tuple in fire-and-forget mode. The counter’s main loop, starting at line 22, receives that message at line 24, increments its counter and waits for the next message.

To find out what the count is, the count function at line 11 sends off a tuple with the symbol count and your process ID, and waits for the counter to report.

This probably isn’t the world’s most idiomatic Erlang, but I think it illustrates message-passing pretty clearly.

In Clojure · You’d use an agent, which is a reference to some data; here’s an example from Stuart Halloway’s Programming Clojure book. First, you get a reference to the agent:

(def counter (agent 0))

Then, to increment it, you send it a function; since Clojure has a built-in inc function, this is pretty easy.

(send counter inc)

Now, you could have sent any old function over there that could apply to whatever counter referred to, in this case an integer, and have that function applied; you can send arguments too. Clojure takes care of maintaining a thread pool and sequencing the messages to the agent. If there’s a chance that the function might block, you can use send-off rather than send ; it uses an expandable thread pool.

Anyhow, to find the current value, you just ask; either of these will do.

(deref counter) @counter

Different Strokes · The differences between these two approaches are extremely interesting. I’m wondering if the class of things you can do with one but not the other is very big or interesting; I suspect that both can be made isomorphic to the Actor model of computation, although the mapping from Erlang is more direct.

[Update:] Thanks to Phil Hagelberg for this comment, which links to a detailed discussion of the Clojure/Erlang trade-offs hosted by Bill Clementson. I see lots of stuff to argue about there...

I’ll dig into this some more (I’m doing a Wide Finder in Clojure to get some hands-on), but I place a certain value on first impressions and thought it might be useful to capture some.

This Is Important · Not all the practical sane lock-free solutions to the concurrency conundrum involve message-passing; for example, see Clojure’s Atoms and Refs. But I do think that message-passing is important; it presents a relatively simple mental model that isn’t terribly hard to think about, it scales naturally outside the shared-global-memory space, and it maps reasonably well onto the semantics actually provided by the underlying operating systems.

Ceremony · Clojure has a lot less. You don’t have to create threads or take care of sending and receiving messages. That counter function involves three lines of code, as opposed to 29 in Erlang. This is good.

To be fair, a user of the Counter module would only ever have to say incr(Counter) and count(Counter) . But still, the whole implementation of both sides is just three lines of Clojure.

I do wonder if there are cases where I’d miss Erlang’s direct control over the process inventory? Sometimes I might be counting twelve things and sometimes twelve thousand.

Coupling · Erlang has a lot less. I don’t need to know what kind of a thing I’m talking to, the messages I send it are just chunks of data, and it gets to decide how to deal with them. In particular, it can trivially ignore those it doesn’t care about.

This seems good to me. But maybe I’m kidding myself here; As a developer, normally wouldn’t I be controlling both sides of the conversation? And as I look at actual Erlang code, I notice that in practice, a lot of the messages are along the lines of { opcode, argument, argument, ... } — they smell a lot like dressed-up function calls.

Pattern Matching · I think this is actually the important difference between the approaches. The message you send to a Clojure agent is just a function, and the system goes about applying it like any other. On the other hand, the receiving end in Erlang gets to pattern-match, which feels natural and looks readable. Is this the proverbial Computer Science one-more-level-of-indirection which turns out to solve an important problem?

Scaling · My use-case here, counting integers, is awfully limited. I’ve built up a “feel” for what Erlang message wrangling feels like in practice at significant-program scale. It’s one of the reasons I’m positive on Erlang: it’s easy to explain and easy to understand (well, compared to locking). When I stress Clojure a bit, if I find it even easier, that’d be a big selling point.

If only Clojure weren’t a Lisp...