Macros in Clojure (and in other Lisps) are an elegant and powerful tool that allows programmers to add new layers of abstraction, to the point that it is seemingly an entirely new language. Macros extend the Clojure compiler to user code, thus enabling developers to programmatically modify program code before evaluation. It is interesting to note that a good part of the Clojure language itself is written with macros. One of the most important things to learn when starting with macros is when and when not to use them. In this article, we explore the what and why of the macro system and how it can be leveraged to grow new language features on top of Clojure.
To establish a firm understanding of macros, two things must be thoroughly understood — namely Clojure’s philosophy of treating code as data (a principle known as homoiconicity),
and Clojure’s runtime.
Code as Data
Clojure is homoiconic, which means that codes written in the language are encoded as data structures. (In other words, code and data have the same representation.)
Consider the sample code below:
(map char (range (int \a) (inc (int \z)) ) )
This code can be evaluated to yield some data which is a sequence of all characters from a to z.
Clojure uses parentheses to denote lists; hence, this code can also be interpreted as a list of three elements:
- A symbol named map
- A symbol named char
- Another list containing three elements: a symbol, and two other lists which can also be further segregated accordingly.
Now let’s consider the following two lines of code that express the two foregoing interpretations in Clojure:
(+ 2 3 ) '( + 2 3 )
The first expression yields the value 2, and the subsequent expression displays the list (+ 2 3 ). This piece of code can be expressed as code or data, depending on how we tell Clojure to interpret it.
The Clojure Runtime
Clojure’s runtime operates in two phases: the read phase, and then the evaluation phase. In the read phase, the Clojure reader looks at the source code as a stream of characters — and since code is data, proceeds to convert these streams into data structures. This is then followed by the evaluation phase, which evaluates the resulting data structures to execute the program.
Macros work by offering a computational hook between the read and evaluation phases of the Clojure runtime, thereby enabling us to arbitrarily generate and transform expressions (a programming technique known as metaprogramming).
Macro definition in Clojure is similar to the definition of a function. Here is its syntax from the Clojure docs.
clojure.core/defmacro ([name doc-string? attr-map? [params*] body] [name doc-string? attr-map? ([params*] body) + attr-map?])
There is a name, an optional document string, an optional attribute map, an argument list, and a body.
While superficially similar to functions, macros and functions differ a great deal. For one, they are internally tagged as macros, not functions. Also, instead of returning a value as functions do, macros return S-expressions that are in turn evaluated to return a value.
Let’s Create a Feature: Borrowing Some Rubies from Ruby
Having solidified our understanding of the basics of macros, let’s get into some action by putting a macro to practical use.
The Ruby programming language provides an unless conditional as part of its decision statements.
It takes a test expression and a body that is only evaluated when the test expression evaluates to false.
Clojure doesn’t provide unless. So, let’s add it. For pedagogical reasons, let’s start with a less idiomatic solution.
Here is our unless macro:
(defmacro unless [test & body ] (list 'if (list 'not test) (cons 'do body ) ) )
Arguments to defmacro are not evaluated before being passed to the macro. The macro takes the arguments supplied as-is and substitutes them accordingly in the code that is generated.
Our unless macro generates a list of code that constructs an if form with a test and wraps the body in a do form.
To have a look at the code that is generated, we can use the macroexpand function. But before we try that, let’s go to town with our new feature.
(unless (= "john" "jo" ) (println "Two different folks" ))
The result on my screen:
Two different folks nil
And another example:
(unless (= "john" "john" ) (println "Two different folks" ))
Great! It works as expected. Now let’s expand our first example to check out the code that’s generated.
(macroexpand '(unless (= "john" "john" ) (println "Two different folks" )))
(if (not (= "john" "john")) (do (println "Two different folks")))
Awesome! The macro is expanded as desired. We just added our first feature. Now let’s add another one — But before we do that, a more idiomatic solution would have been to use the quote(`), unquote(~) and the splice(@) templating characters which makes for code with better clarity in large nested generated S-expressions instead of using the list symbol. Here is how:
(defmacro unless2 [test & body ] `(if (not ~test) (do [email protected])))
As you can see, it’s less nebulous and more clean.
Undoing the Awkwardness: Adding an Infix Function
We are accustomed to writing expressions with the operators in between the operands. So to add 2 to 3, we will mathematically express it as 2 + 3. This is called infix notation. Clojure uses prefix notation where the operator goes before the operand (+ 2 3 ).
Let’s create a macro that will take an infix expression and generate the prefix for Clojure to evaluate. In keeping with simplicity, we will give a fairly naive implementation.
(defmacro infix [& expr] (let [ [left op right] expr] (list op left right)))
Simple! We just had to generate code that will rearrange the operator and the operands back to prefix notation. This implementation is simplified to support only two terms, and does not perform any error checking.
Now let’s try some examples.
(infix 2 - 8 )
-6 (infix 2 + 4 )
It works like a charm.
Now, let’s expand the second example:
(macroexpand '(infix 2 + 4 ))
(+ 2 4)
Great! The terms are rearranged as expected.
We have just added two brand new features to the Clojure language.
When you find yourself in a situation where you feel a particular construct will better express something in the problem domain you are dealing with, but Clojure seems to lack it, don’t sit and hope it pops up in the next version.
Just go ahead and add it yourself, pal!