Another Go at Explaining The Thrush Operator In Clojure

I was recently at a dojo where a few people asked what on earth those -> and ->> symbols did, so I thought I’d have another go at trying to explain them.  You can find my C# heavy first go here.

To start with, (-> a b c d) in clojure expands to (d (c (b a))).  You could describe this as reversing the arguments but I don’t find that very helpful.  I find it easier to think about it imperatively:

  • start with a
  • do b to it
  • do c to the result
  • do d to the result of that

Or, to express it another way (-> a b c d) is equivalent to:

(let [r0 a
r1 (b r0)
r2 (c r1)
r3 (d r2)]
r3)

However, unlike the two alternative forms above, (-> a b c d) is pretty concise and doesn’t involve a lot of brackets.

Composing Functions

Another way to think of it is like a function composition operator.  Imagine you had code like the following:

(map move predators)

But then you wanted the predators to think before moving.  Naively, you could write

(map move (map think predators))

Now, Clojure does have a perfectly sensible function composition operator, so you could write

(map (comp move think) predators)

but even this disguises the fact that you wanted think to occur before move.

(map #(-> % think move) predators)

Multiple Parameters

So, if you thread through single parameter functions, -> is effectively the same as comp with the arguments reversed.  However, if you had functions with multiple parameters, getting comp to do what you want would involve a lot of lambda function.  However, -> is a macro and has some more tricks up its sleeve.  With multiple parameters, -> puts in the argument as the first parameter of the function.*

So (-> p (a b) (c d)) expands to (c (a p b) d).  This may look weird, but it helps if you put your OO hat back on and remember that a lot of functions take “the object” as the first parameter and return a mutated object.  For instance, the jQuery home page contains the following code:

$("p.neat").addClass("ohmy").show("slow");

This could be written in clojure as

(-> 
($ "p.neat")
(addClass "ohmy")
(show "slow"))

In general terms, if I’m doing anything even slightly complex, I’ll put each form on a separate line for clarity.

*Formally, it inserts it as the second form, a distinction that I wouldn’t worry about at this point.

So what does ->> do, then?

The there’s only one difference between -> and ->>: where it inserts the result into the next function.  -> puts it in as the first parameter, ->> puts it in as the last.  So (-> a (b c)) is equivalent to (b a c) while (->> a (b c)) is equivalent to (b c a)).  This make ->> feel a lot more like you’re using “partial” on every line.  You’re pretty likely to need to use ->> if you’re dealing with sequences, because pretty much all of the clojure sequence functions take the sequence as the last parameter.  Here’s a simplified example from the dojo code.

(->> animals
(filter surviving?)
(map move)) 

So, the code says “take the animals, strip out the ones that didn’t make it, then move the rest”.

If you just thread through single parameter functions (or macros), there’s no difference between -> and ->>.

Using the Thrush Operator

I think the thrush/threading operators make people uncomfortable because it’s the first time they encounter the power of macros in Clojure.  However, it’s a really useful tool that can make your code significant more concise.  As with all concise code, you need to find the correct balance with readability.  The more complex your intermediate expressions are, the more likely you are to need a let block to clearly express what you’re trying to do.

Technorati Tags: ,

Published by

Julian Birch

Full time dad, does a bit of coding on the side.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s