/ CLOJURE, ARROW, THREADING

Learning Clojure: the arrow and doto macros

For me, learning a new language is like getting into the sea: one toe at a time.

Last week was the occasion to get familiar with the spec library. This week, we will have a look at some powerful macros.

The problem

When you’re not used to Clojure, parentheses may sometimes impair the readability of code.

(- 25 (+ 5 (* 3 (- 5 (/ 12 4)))))

The Kotlin equivalent of the above snippet would be:

25 - (5 + (3 * (5 - (12 / 4))))

Obviously, it’s related neither to Clojure nor to parenthesis. Let’s rework the above Kotlin snippet using a "data pipeline" to improve the readability:

4.let { 12 / it }
 .let { 5 - it }
 .let { 3 * it }
 .let { 5 + it }
 .let { 25 - it }

The solution

I’m pretty sure presenting the processing in a sequential way improves readability a lot, especially as the number of operations increases. Wouldn’t it be great if Clojure could provide the same sequential processing feature? Fortunately, it does, using a specific macro:

(->> 4    (1)
  (/ 12)  (2)
  (- 5)   (2)
  (* 3)   (2)
  (+ 5)   (2)
  (- 25)) (2)
1Starting from this value
2Apply the function with the result of the previous line as the last parameter

Compared to Kotlin, the only structural difference is that the it parameter is implicit, and used as the last argument.

->> is a macro, called the thread-last macro.

Macros

Clojure has a programmatic macro system which allows the compiler to be extended by user code. Macros can be used to define syntactic constructs which would require primitives or built-in support in other languages. Many core constructs of Clojure are not, in fact, primitives, but are normal macros.

— Clojure documentation
https://clojure.org/reference/macros

Macros are very powerful language constructs, be it in Clojure or other languages. Used sparingly and wisely, they can definitely make some code excerpts easier to read and write. In other cases, they can break the readability of a codebase faster than you can say the word "macro". Be very cautious and think about it before creating your own.

The great thing about Clojure macros is that it’s possible how they will be interpreted via the macroexpand function. Coupled with the REPL, it’s a great tool in a developer’s hands. For example, let’s use macroexpand to check the result of the above code:

(macroexpand '(->> 4     (1)
                (/ 12)
                (- 5)
                (* 3)
                (+ 5)
                (- 25)))
1A single quote before an open parenthesis prevents Clojure from interpreting it as the start of an expression, but rather as a collection.

As would be expected, it yields:

=> (- 25 (+ 5 (* 3 (- 5 (/ 12 4)))))

More macros

There are tons of available macros in Clojure. Among them, some are pretty closely related to the thread-last macro above:

Thread-first

While the ->> macro uses the result of the previous expression as the last argument, the thread-first -> uses it as the first argument. Depending on the expected types, it can either:

  • Prevent the code running because of types mismatch
  • Change the returned result
  • Keep the same result (i.e. for commutative functions, such as additions)

For example, let’s change the thread-last macro of the above snippet to a thread-first and check what each line yields:

(-> 4
  (/ 12)  (1)
  (- 5)   (2)
  (* 3)   (3)
  (+ 5)   (4)
  (- 25)) (5)
11/3
2-14/3N
3-14N
4- 9N
5-34N
Do to

In the above example, arrow functions (or threading functions) are similar to let. They both apply a function and return the result - the only difference being the position of the implicit parameter. Though Clojure is a Functional-Programming language, working on mutable references is sometimes desirable. In general, this is the case when integrating with Java.

In Kotlin, the apply function would be used; in Clojure, the equivalent is doto:

(doto (HashMap.)
  (.put :a "Alpha")
  (.put :b "Beta"))   (1)
1{:b "Beta", :a "Alpha"}

Using the macroexpand reveals the final form:

(macroexpand '(doto (HashMap.)
  (.put :a "Alpha")
  (.put :b "Beta")))

(let* [G__1755 (HashMap.)]   (1)
  (.put G__1755 :a "Alpha")  (2)
  (.put G__1755 :b "Beta")   (2)
  G__1755)                   (3)
1Create a new instance of HashMap under a random reference name
2Put data in the map
3Return the map

Conclusion

In this post, we saw some specific macros that are quite useful during development. Arrow functions are similar to Kotlin’s let while doto is akin to apply.

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with narrower interests like Software Quality, Build Processes and Rich Internet Applications. Currently working for Exoscale. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More