Threading Macros in Clojure
Threading macro in Clojure is a powerful construct which aids in improving the readability of code multi-fold. It kind of aligns with the way our brains think about and understand a process or a data flow. These macros, at a basic level, are syntactic sugar. They, however are very useful and enjoyable to use.
Threading macros convert nested function calls into a linear flow enabling one to visualise and easily understand as to how the data is flowing from one call to the other.
Two fundamental types of Threading macros
in Clojure are as below :
a. Thread first
— (-> x & forms)
b. Thread last
— (->> x & forms)
The difference is basically in the way input data is positioned as it flows from one function call to the other.
Let us work through some examples to understand better.
In a Lisp based language like Clojure, the code is kind of evaluated from the inside out
as you may be aware. The innermost form is evaluated first and the result is combined with the immediate outer form and the process continues till a final result is obtained. Let us look at contrived example below :
(reduce + (map #(* % %) (filter odd? (range 10))))
;; 165
What the above does, in plain english, is this :
- first, get the list of
numbers from 0 to 9
- pick out only the ones that are
‘odd’
square
them all- finally,
sum
the resulting values
However, the code represented isn’t very readable, right ? It isn’t tuned to the way we would understand and describe it. This is where threading macros
come into play.
The above code written using a threading macro would look like this :
(->>
(range 10)
(filter odd?)
(map #(* % % ))
(reduce +))
;; 165
Looking at the above code line by line, it is pretty clear as to what is going on. The result at each step is fed into the next as input
. This is the simplicity and power of threading macros.
The example above is what is known as the Thread last
macro. In Thread last
, the result of every line is passed to the subsequent line as the last parameter
. The ‘commas
’ in the code below indicate the position at which the input data is inserted (the commas are ignored by the Clojure compiler).
(->>
(range 10)
(filter odd? ,,,)
(map #(* % % ) ,,,)
(reduce + ,,,))
;; 165
In Thread first
however, the input data is positioned between the function name and the first argument ( so, effectively as the first
parameter
in every subsequent step)
Let us look at an example
(->
"HeLLo"
(clojure.string/lower-case)
(clojure.string/index-of "e"))
;; 1
Here, the third line (which tries to find the ‘index’ of the character ‘e’
in the string) needs the input string as the first parameter ( between the function name and the parameter ‘e’). This is ‘Thread first
’
Once again, the ‘commas
’ below indicate the position of the input data.
(->
"HeLLo"
(clojure.string/lower-case ,,,)
(clojure.string/index-of ,,, "e"))
;; 1
Now, let us look at some examples where we intersperse thread first
and thread last
Let us try and fetch a file
named common-english-words.txt
(which contains, as the name suggests, the most commonly found words in the english language) from the internet and work with it.
Below, we use thread first
to :
- download the file and read the contents into memory as a string
- split the string on
,
as the separator, resulting in a vector of all the words
(-> (slurp "https://www.textfixer.com/tutorials/common-english-words.txt")
(clojure.string/split ,,, #","))
;; ["a" "able" "about" "across" "after" "all" "almost" "also" "am" "among" "an" "and" "any" "are" "as" "at" "be" "because" "been" "but" "by" "can" "cannot" "could" "dear" "did" "do" "does" "either" "else" "ever" "every" "for" "from" "get" "got" "had" "has" "have" "he" "her" "hers" "him" "his" "how" "however" "i" "if" "in" "into" "is" "it" "its" "just" "least" "let" "like" "likely" "may" "me" "might" "most" "must" "my" "neither" "no" "nor" "not" "of" "off" "often" "on" "only" "or" "other" "our" "own" "rather" "said" "say" "says" "she" "should" "since" "so" "some" "than" "that" "the" "their" "them" "then" "there" "these" "they" "this" "tis" "to" "too" "twas" "us" "wants" "was" "we" "were" "what" "when" "where" "which" "while" "who" "whom" "why" "will" "with" "would" "yet" "you" "your"]
again the commas
here ,,,
indicate the position for the input at that step.
If, say, in the above scenario, you would want to convert all the words to uppercase
for some strange reason, the code will be something like this
(-> (slurp "https://www.textfixer.com/tutorials/common-english-words.txt")
(clojure.string/split ,,, #",")
(map #(clojure.string/upper-case %)))
post splitting based on ,
we would need to map
the method upper-case
from the clojure.string
library onto each of the element in the vector. However, there is a slight problem we run into.
(-> (slurp "https://www.textfixer.com/tutorials/common-english-words.txt")
(clojure.string/split ,,, #",")
(map ,,, #(clojure.string/upper-case %)))
;; Error printing return value (IllegalArgumentException) at
clojure.lang.RT/seqFrom (RT.java:557).
Don't know how to create ISeq from: user$eval2164$fn__2165
this is because, at the last step in the process, the vector
created is inserted in-between map
and #(clojure.string/upper-case %)
as we are working with thread first
This doesn’t work for us in this scenario. We need the parameter to be placed last, that would be thread last
right.
So, we can jump into a thread last
context just for that statement alone like this :
(-> (slurp "https://www.textfixer.com/tutorials/common-english-words.txt")
(clojure.string/split ,,, #",")
(->> (map #(clojure.string/upper-case %))))
;; ("A" "ABLE" "ABOUT" "ACROSS" "AFTER" "ALL" "ALMOST" "ALSO" "AM" "AMONG" "AN" "AND" "ANY" "ARE" "AS" "AT" "BE" "BECAUSE" "BEEN" "BUT" "BY" "CAN" "CANNOT" "COULD" "DEAR" "DID" "DO" "DOES" "EITHER" "ELSE" "EVER" "EVERY" "FOR" "FROM" "GET" "GOT" "HAD" "HAS" "HAVE" "HE" "HER" "HERS" "HIM" "HIS" "HOW" "HOWEVER" "I" "IF" "IN" "INTO" "IS" "IT" "ITS" "JUST" "LEAST" "LET" "LIKE" "LIKELY" "MAY" "ME" "MIGHT" "MOST" "MUST" "MY" "NEITHER" "NO" "NOR" "NOT" "OF" "OFF" "OFTEN" "ON" "ONLY" "OR" "OTHER" "OUR" "OWN" "RATHER" "SAID" "SAY" "SAYS" "SHE" "SHOULD" "SINCE" "SO" "SOME" "THAN" "THAT" "THE" "THEIR" "THEM" "THEN" "THERE" "THESE" "THEY" "THIS" "TIS" "TO" "TOO" "TWAS" "US" "WANTS" "WAS" "WE" "WERE" "WHAT" "WHEN" "WHERE" "WHICH" "WHILE" "WHO" "WHOM" "WHY" "WILL" "WITH" "WOULD" "YET" "YOU" "YOUR")
So, jumping from thread first
to thread last
and back is quite straightforward and very useful.
The same thing, if needed in the reverse, like starting with thread last
and jumping into thread first
in the middle and back is also possible, but needs a small trick to achieve.
Let us look at one such example :
In the above scenario with the list of common words, the first thing we did was to read the file and split it into a vector of individual words. The split
method actually needs the input string to be passed as the first
parameter
. This is a good case for us to explore.
We start with thread last
then, to execute the split
we switch to thread first
by just creating an anonymous function which helps us to specify the position of the input parameter with a %
. We then switch back to thread last
for counting the number of words. This is more of a stealthy way of working by tricking the compiler. As you might notice, we are not explicitly using the thread first
macro ->
here.
(->> (slurp "https://www.textfixer.com/tutorials/common-english-words.txt")
(#(clojure.string/split % #","))
count ,,,)
;; 119
threading macros
are not only very enjoyable to work with but also are very powerful and improve the overall readability of the code.
Hope that gave a decent primer to these constructs.
There are other more specialised threading macros
in Clojure as well, namely some->
some->>
and cond->
. May be a topic for another article. Until then, adios.