/ 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.

This is the 3rd post in the Learning Clojure focus series.Other posts include:

  1. Decoding Clojure code, getting your feet wet
  2. Learning Clojure: coping with dynamic typing
  3. Learning Clojure: the arrow and doto macros (this post)
  4. Learning Clojure: dynamic dispatch
  5. Learning Clojure: dependent types and contract-based programming
  6. Learning Clojure: comparing with Java streams
  7. Feedback on Learning Clojure: comparing with Java streams
  8. Learning Clojure: transducers

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)
1 Starting from this value
2 Apply 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)))
1 A 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)
1 1/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)
1 Create a new instance of HashMap under a random reference name
2 Put data in the map
3 Return 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

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 focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Learning Clojure: the arrow and doto macros
Share this