Understanding Basics of Clojure Macros
What are Macros
Macros are a key feature in Lisp family, and you've probably been using them for a while now. Some of the functions that you've been using in clojure.core
namespace are actually macros, i.e. they are defined using defmacro
instead of defn
. The main purpose of a macro is to manipulate code into new code (i.e. metaprogramming), which leverages on the homoiconicity of Clojure (data is code, code is data). In essence, it adds new syntax into the language. A good example is when
macro, which is made up of if
and do
. Clojure replaces the when
expression when compiling your code, into the if
and do
s, before executing them. This step is called macro expansion.
We can see it in action.
Let's defined a simple greeting macro:
;; Don't mind the ` and ~ syntax for now
(defmacro say-hi [name]
`(str "Hello " ~name))
(say-hi "John") ;; "Hello John"
To see what say-hi
is changed into during compile time, we can use macro-expand
:
(macroexpand '(say-hi "Hobs"))
;; (clojure.core/str "Hello " "Hobs")
As you can see, it just simply converts into str
function call. This effectively creates a new syntax for Clojure: say-hi
. At this point, this seems completely unnecessary, after all, you could just define a normal function using defn
that does the exact same thing. Why bother writing a macro? What's the value there?
Shifting work from runtime to compile time
Let's look at this slightly modified example:
(defmacro say-hi-compile-time [name]
(str "Hello " name))
(say-hi-compile-time "George") ;; "Hello George"
(macroexpand '(say-hi-compile-time "George")) ;; simply "Hello George" - no more `str` function
Now this say-hi-compile-time
looks a lot like the say-hi
above, but there is a key difference - after compilation, the calls to (say-hi-compile-time "George")
is simply replaced to string value "Hello George"
, no more calls to str
at runtime!
The quotes
Now some of the syntax above may seem quite confusing. Specifically, the `, ', ~, ~@.
Quotes mean that I don't want Clojure to evaluate the code. You probably used this to define a list without using list
function.
;; They all evaluate to (1 2 3)
(list 1 2 3)
`(1 2 3)
'(1 2 3)
;; if you don't quote, then Clojure will try to call a function 1 since it's at the beginning position of parenthesis, which doesn't exist.
(1 2 3) ;; java.lang.Long cannot be cast to clojure.lang.IFn - i.e. 1 is not a callable function
Now, let's say you have defined a value result
to be 3:
(def result 3) ;; define value 3 to symbol result
And you want to construct the same quoted list again, it gives you the symbol result
, not evaluating it to value 3
(list 1 2 result) ;; (1 2 3) same as before
`(1 2 result) ;; (1 2 file-name-space/result)
'(1 2 result) ;; (1 2 result)
Now, if you want to evaluate result
inside the quote, you will need to unquote
it:
(list 1 2 ~result) ;; error, doesn't understand unquote when not inside a quote
`(1 2 ~result) ;; (1 2 3) - usually this is what we want in writing macro
'(1 2 ~result) ;; (1 2 ~result) - ~ operator is not evaluated
In addition, if you want to splice a sequence in side quote, you can use ~@:
(def results [3 4])
`(1 2 ~@results) ;; (1 2 3 4)
These bunch of syntaxes are actually part of Clojure reader macro, some others you already know include ;
for comments, and @
for dereferencing. You can replace them with functions:
; I am a comment
(comment "I am a comment")
(def x (atom 1))
@x ;; 1
(deref x) ;; 1
There are many more and I'm sure you'll recognize some of them!
Summary
All these songs and dances with defmacro
and quoting / unquoting are to serve one purpose - get the result of a macro into a list so that Clojure can evaluate that list. In other words, writing macros is just building lists for evaluation.