Clojure threading macros
02 Oct 2017A Threading Macro in Clojure is a utility for representing nested function calls in a linear fashion.
Simple transformations
Meet mick
.
user=> (def mick {:name "Mick" :age 25})
#'user/mick
He’s our subject for today.
If we wanted to give mick
an :occupation
, we could simply do this using assoc
; like so:
user=> (assoc mick :occupation "Painter")
{:name "Mick", :age 25, :occupation "Painter"}
At the same time, we also want to take note of his earning for the year:
user=> (assoc mick :occupation "Painter" :ytd 0)
{:name "Mick", :age 25, :occupation "Painter", :ytd 0}
Keeping in mind that this isn’t actually changing mick
at all. It’s just associating new pairs to him, and returning the new object.
mick
got paid, $100 the other week, so we increment his :ytd
by 100. We do this by performing the transformation after we’ve given him the attribute.
user=> (update (assoc mick :occupation "Painter" :ytd 0) :ytd + 100)
{:name "Mick", :age 25, :occupation "Painter", :ytd 100}
He earned another $32 as well, in another job.
user=> (update (update (assoc mick :occupation "Painter" :ytd 0) :ytd + 100) :ytd + 32)
{:name "Mick", :age 25, :occupation "Painter", :ytd 132}
He also got a dog.
user=> (assoc (update (update (assoc mick :occupation "Painter" :ytd 0) :ytd + 100) :ytd + 32) :pets [:dog])
{:name "Mick", :age 25, :occupation "Painter", :ytd 132, :pets [:dog]}
So, the nesting gets out of control. Quickly.
Thread first macro
We’ll use ->
(The thread-first macro) to perform all of these actions in one form (must as we’ve done above), but in a much more readable manner.
user=> (-> mick
#_=> (assoc :occupation "Painter" :ytd 0)
#_=> (update :ytd + 100)
#_=> (update :ytd + 32)
#_=> (assoc :pets [:dog]))
{:name "Mick", :age 25, :occupation "Painter", :ytd 132, :pets [:dog]}
So, it’s the same result; but with a much cleaner and easier to read interface.
Thread last macro
We saw above that the ->
threading macro works well for bare values being passed to forms. When the problem changes to the value not being supplied in the initial position, we use thread last ->>
. The value that we’re threading appears as the last item in each of the transformations, rather than the mick
example where they were the first.
user=> (filter #(> % 12) (map #(* % 5) [1 2 3 4 5]))
(15 20 25)
We multiply the elements of the vector [1 2 3 4 5]
by 5
and then filter out those items that are greater than 12
.
Again, nesting quickly takes over here; but we can express this with ->>
:
user=> (->> [1 2 3 4 5]
#_=> (map #(* % 5) ,,,)
#_=> (filter #(> % 12) ,,,))
(15 20 25)
Again, this is a much more readable form.
as
If the insertion point of the threaded value varies, we can use as->
to alias the value.
user=> (as-> "Mick" n
#_=> (clojure.string/upper-case n)
#_=> (clojure.string/reverse n)
#_=> (.substring n 1))
"CIM"
Take the name “Mick”
- Convert it to upper case
- Reverse it
- Substring, skipping the first character
It’s the substring
call, which takes the string in the initial position that’s interesting here; as it’s the only call that does that. upper-case
and reverse
take it in as the only (or last).
some
The two macros some->
and some->>
work like their ->
and ->>
counterparts; only they do it on Java interop methods.
cond
cond->
and cond->>
will evaluate a set of conditions, applying the threaded value to the front to back of any expression associated to a condition that evaulates true.
The following example has been taken from here.
(defn describe-number [n]
(cond-> []
(odd? n) (conj "odd")
(even? n) (conj "even")
(zero? n) (conj "zero")
(pos? n) (conj "positive")))
So you can describe a number as you go:
user=> (describe-number 1)
["odd" "positive"]
user=> (describe-number 5)
["odd" "positive"]
user=> (describe-number 4)
["even" "positive"]
user=> (describe-number -4)
["even"]