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:
- Decoding Clojure code, getting your feet wet
- Learning Clojure: coping with dynamic typing
- Learning Clojure: the arrow and doto macros (this post)
- Learning Clojure: dynamic dispatch
- Learning Clojure: dependent types and contract-based programming
- Learning Clojure: comparing with Java streams
- Feedback on Learning Clojure: comparing with Java streams
- 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.
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
.