← Back to posts

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 dos, 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.

HomeBlogContactGithubReading