Readable Clojure Through Threading
Clojure, like many Lisps, sometimes struggles to attract newcomers who claim it's "hard to read". Any paradigm shift requires time, but I myself struggled to read Clojure I had written early on. Nested parentheses and REPL-driven development made the result come quickly, but it often looked ugly. However, the thread operator
-> and all of its cousins fix that.
A common pattern I found myself reapeating was a series of simple, composable function calls on a single value. After all, small useful functions is a big draw of Clojure. But many small functions next to one another often results in ugly and unclear code.
For example, in many ciphers, strings first need to be converted to lowercase, then stripped of the whitespace characters. The quick way to do that in Clojure isloading
This isn't too hard to read, but it gets more difficult as functions are added. If it's determined that somehow the cipher is made stronger by reversing the string, this becomesloading
Already this is getting a bit unwieldy. The thread operator simplifies this to:loading
The thread operator here inserts
s as the argument to
str/lower-case, then inserts that entire form,
(str/lower-case s), as the first argument in
str/replace, and so on. As a result, it's functionally equivalent, but now any humans reading it can see clearly that you would take the string, first lower-case it, then replace the whitespace, then reverse.
But, when we start working with collections, we see that we need something new.
-> threads things as the first argument to a function, while most functions that deal with collections take the collection last. Here, we want to use the
->> operator, also known as the thread-last operator.
Again, using our cipher example, let's say we have a function that takes a string, filters all characters removing whitespace, converts a character to an integer, and applies an encoding function. In the inside-out style, this would beloading
However, with the
->> operator, we can simplify this to
These two operators alone will simplify and clarify a lot of Clojure functions, but there are a few more obscure threading operators that can be very useful.
some->. The some threading macro can be thought of as a short-circuit, or nil-safe threading. With
some->, whenever the result of one line is nil, the expression immediately returns nil.
For example, code that would imperatively be written asloading
can instead be refactored to beloading
Next on the obscure threading macros is
cond->. The conditional threading macro works much like
cond, but with the addition of threading. It takes an even number of forms, and for each pair, if the first is true, execute the second according to usual threading rules.
For example, if you have a series of functions that only need to be executed under certain circumstances, instead ofloading
we can instead write this asloading
I personally like using
cond-> as a way to conditionally associate elements in a map. When building up a body for a request, a common pattern might be
It is worth noting that both
some-> have thread-last versions as well,
The last threading macro is
as->, which only comes in one type.
as-> is useful if you want to mix thread first and thread last macros.
For example, let's say we don't know about the ability to filter, as we did in the
->> example. That might lead us to write the function as
It would not take much to make this even more complex. Instead, we can use
as->, which instead of passing things as the first or last argument, assigns the previous form to a symbol for use in future expressions. For example, the previous function can be rewritten as
Now, you're fully equipped to simplify your code with threading macros. It's worth noting that I left out one big detail for stylistic reasons, namely that parenthesis are optional when referring to a single function. I prefer always adding them, as it results in the expressions lining up.
As with any technique, you can overuse this and make your code just as unreadable through long and complicated threading macros. But when used judiciously, they enhance readability.