Target Practice with the Arrow Macro

Target Practice with the Arrow Macro

Sometimes I end up with rather ugly looking code that shoots to the far right and looks like a cliff hanging over the code below. While the line works, it certainly does not looks nice and can be a pain to decipher in the future. Here is an example of what I am talking about, albeit contrived:

(keyword (.toUpperCase (.concat (.replace (.trim " cat ") "c" "m") "dog"))))

In terms of readability, this is not ideal. Fortunately, there happens to be a macro in Clojure that can help make this look better. I will call this the arrow macro as it is written ->, but is commonly known as the thread macro or even the thrush macro. I am hesitant to call it either as 1) it doesn't look like a thread, and 2) to avoid the implications that it involves concurrency as it does not. Of course, calling it an arrow could imply morphism from category theory, but let's ignore that. Besides, a wonderful book, The Joy of Clojure, calls it an arrow macro.

Practice Makes Perfect

In a nutshell, macros let you transform an expression, before runtime, into something else. There is a staggering amount of possibilities within this concept alone. I won't be able to do sufficient justice on the topic at the moment, but there are many resources available to learn more.

Fortunately, you don't need to know about macros to make use of ->. The arrow macro is of the form (-> x & forms). It threads the x as the first argument of the first form and then uses the result as the first argument of the next form and so on.

Here are a few examples of -> in action.

(-> 4.5
    -
    int
    str)
;=> "-4"

(-> [1 2 3]
    first
    (str " is best"))
;=> "1 is best"

(-> {:a 1 :b 2 :c 3}
    :b
    (+ 3)
    str 
    keyword)
;=> :5

(-> {:a {:b 2 :c {:d 4}}}
    :a :c :d (+ 3))
;=> 7

(-> "t"
    (str "ca"))
;=> "tca"

While not the same macro, there is another that will insert x as the last argument of the first form and continue inserting the results as the last argument in the forms after, ->>.

(->> "t"
    (str "a")
    (str "c"))
;=> "cat"

Note that functions taking a single argument are not wrapped in parenthesis, but functions of greater arity are. Forgetting the parenthesis when you need them will result in an error - making it easy to catch.

See, the arrow macro is not that difficult to use! But should we always use it?

By all means you don't have to use the arrow macro over traditional composition such as (f (g x)) all the time. Often it feels more appropriate to use one over the other given the context and what you are trying to express. This Stack Overflow answer offers some general guidelines for when to use which.

One common place you will find the arrow macro is routing for Compojure. The macro makes it easy to add or remove middleware.

Hitting the Mark

Now that we have a decent understanding how of the arrow macro works, lets go back to the original function and rewrite it.

(-> " cat "
    .trim
    (.replace "c" "m")
    (.concat "dog")
    .toUpperCase
    keyword)

And there we have it - the function looks a bit cleaner now and is arguably improved, all thanks to a friendly little ->. We can easily see a sequence of operations is being performed and it is trivial to insert or remove additional operations. Next time you have a sequence of operations consider trying the arrow macro and see if it feels right.


Photo by Vince Fleming / Unsplash