Threading Macros in Clojure

Santhosh Krishnamoorthy
6 min readNov 11, 2022

--

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 :

  1. first, get the list of numbers from 0 to 9
  2. pick out only the ones that are ‘odd’
  3. square them all
  4. 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 :

  1. download the file and read the contents into memory as a string
  2. 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.

--

--

Santhosh Krishnamoorthy
Santhosh Krishnamoorthy

Written by Santhosh Krishnamoorthy

Passionate Technologist. Also, a Naturalist and a Nature Photographer. Find my Wildlife & Nature Photography blog @ framesofnature.com

No responses yet