I use Clojure's threading macros perhaps more than I should, but I do so because they mimic the way I think about the code I write. A lisp programme is a tree and yet most of the programmes that we write are, or should be, lines of functions acting on a datastructure1 2.
The value of threading macros
| |
To grok this code I have to start from the bottommost leaf, in this case (/ 2 2), yet it is at the line's far right contrary to where it is natural for me and most of the world3 to start reading. Conversely the same code written with a threading macro mirrors how we think about what the code is doing.
| |
They also force the programmer into writing simpler code because threading macros don't handle trees well.
| |
What a mess. It's much better to split it into two linear forms.
| |
So threading macros help make easy things simple and complex things hard4.
as->
as-> allows one to use a symbol to specify where the threaded value goes in each form. It is helpful when the threaded value's position in the function's arguments changes. Some Clojure functions take the datastructure as the first argument, others take a function.
| |
some->
some-> works similarly to ->, except that evaluation of the form terminates as soon as the threaded value becomes nil. It's named for some? which only returns false if its argument is nil. It is, generally, unnecessary in Clojure. Why? Because Clojure's core functions, and the functions that we write, gracefully handle nil.
| |
The same cannot be said for calling functions in Java. Try adding a value to a null HashMap, or .toString on a null object, and you'll throw a NullPointerException.
| |
If db/find-by-id returns nil, implying a missing entity, the first form will explode when it tries to .put on a null object. The second gracefully creates and saves a new entity. If we still need to deal with the Java method, we can do so by either wrapping the form in a try form, or use some->.
| |
if-some is just like if-let, except instead of the test failing for falsy values, it fails for nil values. Equivalently, the test passes for (some? test) instead of (true? test).
When db/find-by-id returns nil, some-> immediately returns nil without evaluating the remaining forms of the threading macro, .put and db/save. if-some then handles the case of a missing entity. Otherwise, as long as db/save returns any non-nil value, it is bound to save-result and (handle-success save-result) will be called, even if the result is false.
cond->
I have found cond->, named for cond, to be the biggest code-saver out of any of Clojure's rarer standard library functions. Again, I use it mostly when working with Java, specifically with Builders. A Builder is one of the most common patterns you will see in object-oriented languages, it works to progressively build up an object, allowing for each field to have a default value, set fields by more convoluted means5, or verify fields before they are set6.
| |
Of course, if this was Clojure we could just do this:
| |
But occasionally we will need to use a Java-style Builder.
| |
This blows up in ones face if this code get called by someone who wants to use the default value and doesn't want to set foo. In that case f is nil and a NullPointerException is thrown. cond-> is a threading macro that allows us to only evaluate a form if a predicate returns true, and can be used to avoid this style of error.
| |
If the predicate returns true, the next form is used as it would be in ->. Unlike some->, if a predicate fails the macro doesn't terminate, it just moves onto the next predicate.
If (pred-2 b) is the only predicate to fail, it becomes equivalent to this:
| |
We can use this to check that a variable is non-nil before trying to set the field on the Builder.
| |
And finally we call build to return the EnterpriseIntegrator.
Other macros
->, some->, and cond-> each have a matching macro, ->>, some->>, and cond->>, which inserts the threaded value as the last argument in each form. I don't find them often to be of much use.
You can see Clojure's official guide on the subject here.
"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures." — Alan Perlis, Epigrams on Programming
The Language of the System by Rich Hickey, Conj 2012. Having abandoned object-oriented languages, don't recreate them in the large by building a system of components that look like objects. Instead, choose to structure your system like you structure your functional programmes, pipelines of functions.
The major exceptions being Arabic, Hebrew, and Urdu.
| |
| |