magnars

Sewing S-Expressions with Thread-First and -Last

På norsk på Kodemaker-bloggen.

When you’re new to Clojure, the macros -> and ->> can be pretty confusing, making the code look quite mystical. Fortunately, it won’t be long now before you fall in love and start using them everywhere.

What They Look Like and How They Work

The macros -> (thread-first) and ->> (thread-last) let you rewrite deeply nested code into something that looks more like imperative statements.

Take a look at these four code snippets:

(-> player
    (update :health dec)
    (dissoc :happy?)
    (assoc :bloodied? true))
(-> (update player :health dec)
    (dissoc :happy?)
    (assoc :bloodied? true))
(-> (dissoc (update player :health dec) :happy?)
    (assoc :bloodied? true))
(assoc (dissoc (update player :health dec) :happy?) :bloodied? true)

All of these expressions evaluate to the same thing, but most would argue that the first is easier to read. That’s exactly the point. Thread-first -> allows us to write code in a more readable manner, without affecting what the compiler sees.

If you study the examples more closely, you’ll see that -> stitches together the expression by placing the previous element into the next. Where? In the first parameter position. Thread-first.

What about thread-last ->>? Well, now you might be wondering.

Thread Last

Here are some new code snippets:

(->> zombies
     (filter :aware-of-player?)
     (remove :dead?)
     (map move-towards-player))
(->> (filter :aware-of-player? zombies)
     (remove :dead?)
     (map move-towards-player))
(->> (remove :dead? (filter :aware-of-player? zombies))
     (map move-towards-player))
(map move-towards-player (remove :dead? (filter :aware-of-player? zombies)))

Again, the first one is easier to read. The operations are listed in the same order as they are performed. The parentheses don’t feel as overwhelming.

You can see that ->> also stitches the expression together by placing the previous element into the next, but this time in the last parameter position.

And that’s really all you need to know. -> and ->> make your code more readable by avoiding deep nesting. But if you stop reading here, you won’t learn about the secret in clojure.core.

The Secret in clojure.core

When Rich Hickey was creating Clojure, he spent a lot of time in his hammock pretending to sleep. There, he pondered a lot, and much good came out of that thought work. One such gem is how the parameter lists of Clojure’s core functions are secretly designed with threading in mind:

Think back to the examples above. When we worked with player (a map), it was easy to use assoc, dissoc, and update with thread-first ->. Why? Because all these take the map as the first argument.

When we worked with zombies (a list), it was easy to use filter, remove, and map with thread-last ->>. Again, because all these take the seq as the last argument.

I call it a secret because I haven’t seen it explicitly written in the documentation for Clojure anywhere, but all the functions work according to that principle.

But what does it really mean?

Clojure is Full of Affordances

One of the advantages of learning functional programming with Clojure is that it forces your hand. Kotlin, Scala, and Groovy are all fine languages, yet they often allow old habits to persist. When you come to Clojure, it’s a hard stop. You must learn to write functional code to get anywhere.

Clojure, therefore, has opinions on how you should write code. The language makes it pleasant to do things ‘right’ and painful to do them ‘wrong’.

My claim is that thread-first -> and thread-last ->> are set up as such an affordance: Operations on the same data structure are easy to compose together, but the moment you switch between a map and a seq, it becomes painful. You have to switch threading. It’s awkward.

Read: Don’t do it.

Instead, break up the code. When you go from a map to a seq, break the threading. When you go from a seq to a map, break up. It’s okay to give it a name. Point-free programming is cool, but don’t take it too far.

This way, you’re going with the flow of Clojure.

Playing Well with Others

Another very common reason threading becomes awkward is when your own functions don’t follow the same principle. It’s not surprising that this happens, really. It was a secret, after all. But now you’re in on it.

Follow the same principle yourself. Accept maps as the first parameter and seqs as the last, and things will flow better.

Look how nice:

(defn loot-bodies [player zombies]
  ...)

(-> player
    (update :health dec)
    (dissoc :happy?)
    (assoc :bloodied? true)
    (loot-bodies zombies))

Here we accept player first, and it plays along with ->. The other way around would have been awkward:

(defn loot-bodies [zombies player]
  ...)

(loot-bodies zombies
             (-> player
                 (update :health dec)
                 (dissoc :happy?)
                 (assoc :bloodied? true)))

Nope.

Finally, Why Do We Need These?

It’s an interesting question, because it’s easy to think this comes down to Clojure being a Lisp, you know, with all those parentheses. That’s not the case. When writing Emacs Lisp, for example, you write good old-fashioned imperative code: A long series of statements one after the other.

No, the reason is that Clojure code is built up of expressions - not statements. The reason is immutability. Without a place to store intermediate state, you are forced to nest expressions. That can quickly reduce readability.

Thread-first -> and thread-last ->> give much of that readability back.