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/mickHe’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"]