ClojureScript, like Clojure, uses macros to extend the syntax of the language. Fundamentally, a macro is just a function that manipulates data structures. What makes macros special is that they are invoked during the compilation process, to manipulate the data structures representing ClojureScript source code. Many of ClojureScript’s core flow-control operators are implemented as macros, and you can write your own macros to extend the language.
Remember from Chapter 4 that all ClojureScript code is composed of data structures: lists, vectors, symbols, and so on. For example:
(println "Three plus four is" (+ 3 4))
We can read this expression as a list containing a symbol, a string, and another list. But to the ClojureScript compiler, that list represents a function call.
Macros allow you to manipulate the data structures in your code before they get to the compiler. This is very powerful: a macro can effectively rewrite code before it gets to the compiler.
Macros are applied during the compilation process. They do not exist at runtime. Because the ClojureScript compiler is implemented in Clojure, ClojureScript macros must be written in Clojure, not ClojureScript. Fortunately, Clojure and ClojureScript are almost identical when it comes to manipulating data structures, so switching between the two languages is not difficult.
As an example, consider the when
macro introduced in
Chapter 4:
(when condition ;;... expressions ...
) ;; which expands to: (if condition (do ;;... expressions ...
))
The when
macro is simply a way to avoid the extra
do
block when we want multiple expressions in an
if
expression. Usually this happens when the code inside the
when
macro is performing side effects.
To write a macro, first think about the expression you want to be
able to write in your code. Second, think about what
you want it to become. Finally, write a function that
converts the first into the second. Here is a simple version of the
when
macro:
(defmacro when [condition & body] (list 'if condition (cons 'do body)))
Notice that a macro definition looks just like a function
definition, but it starts with defmacro
instead of
defn
. This function is variadic: it takes one argument called
condition
followed by any number of arguments that will be
collected into the list called body
. It then constructs a
list starting with the symbol if
, followed by the condition,
followed by body
with the do
symbol inserted at
the head.
Applying a macro is called macroexpansion, and
it happens at the beginning of the ClojureScript compilation process. You
can test it at the REPL with the macroexpand-1
function at
the Clojure REPL. Remember, macros are written in Clojure, not
ClojureScript, so you must write and test them at the Clojure REPL.
user=> (macroexpand-1 '(when (even? 2) (println "2 is even"))) (if (even? 2) (do (println "2 is even")))
Notice that we are calling the macroexpand-1
function
on a quoted form. We don’t want to
evaluate the when
expression; we want to
see what it will expand to during compilation. The
macroexpand-1
function performs one round of macroexpansion.
However, a macro can expand to code, which begins with another macro. To
see the final result of all the expansions, you can call the
macroexpand
function, which keeps expanding macros until it
reaches an expression that is not a macro.
There is also macroexpand-all
, which recursively
expands all the macros anywhere in an
expression. It is available in the Clojure namespace
clojure.walk
. This macroexpand-all
is not
entirely correct because it doesn’t recognize special forms such as
let
, but it is usually adequate for debugging macro
expressions.
Macros manipulate data structures that represent code. However, as
the code they produce grows more complex, it becomes tedious to manually
construct the data structures to represent it. To help, Clojure has the
syntax-quote operator to construct “templates” for
expansion. Syntax-quote is written using the backtick (`
)
symbol. It behaves like the normal single quote in that it prevents
evaluation, but syntax-quote also allows values to be
unquoted.
Here is a version of when
written with
syntax-quote:
(defmacro [condition & body] `(if ~condition (do ~@body)))
Notice that we don’t have to do any manual construction, such as
invoking list
, as in the previous example. Instead, the
syntax-quoted form looks similar to the form we ultimately want to
produce. Within that form, we have unquoted the condition
symbol by prefixing it with a tilde (~
). We have used a
variant of unquote called unquote-splicing on the
body
symbol. The unquote-splicing operator
(~@
) operates on lists by inserting the contents of the
list at the expansion point, without the enclosing parentheses of the
list itself. Unquote-splicing is like “unwrapping” a list before placing
it in the expansion.
It is often necessary to create new symbols in the body of a
macro, such as let
bindings. To prevent these symbols from
clashing with symbols already in use elsewhere around the code,
Clojure’s macros provide auto-gensyms, or
automatically-generated symbols, guaranteed to have unique names. These
symbols are generated by placing a hash sign (#
) after the
symbol name. Auto-gensyms are only available within a syntax-quoted
expression.
For example, here is a macro that expands to some debugging code:
(defmacro debug [expr] `(let [result# ~expr] (println "Evaluating:" '~expr) (println "Result:" result#) result#))
In this example, the debug
macro takes a single
expression and uses it twice. To avoid evaluating expr
more
than once, it has to create an intermediate let
binding.
The result#
symbol will expand to an auto-gensym, which is
guaranteed to have a unique name that doesn’t clash with any other
symbols. Macroexpansion shows the result:
user=> (macroexpand-1 '(debug (println "hello"))) (clojure.core/let [result__6__auto__ (println "hello")] (clojure.core/println "Evaluating:" (quote (println "hello"))) (clojure.core/println "Result:" result__6__auto__))
The debug
macro also contains a clever trick: the
“quote-unquote” in '~expr
. This allows the expansion
to print the literal code of expr
without evaluating
it.
Because macros are written in Clojure, they must be loaded
differently in the ClojureScript compiler. To reference a macro from
another namespace, add it to the ns
declaration using the
:require-macros
form. For example:
(ns my-project.main (:require-macros [my-project.foo :as foo])) (foo/my-macro)
This assumes that a Clojure source file is
available on the classpath at my_project/foo.clj
containing
defmacro foo
.
With the exception of the ns
declaration, you generally
do not need to think about whether you are calling a function or a macro
in ClojureScript code. Many of the core flow-control structures of
ClojureScript are implemented as macros (many of the core ClojureScript
macros are actually the same as the core Clojure macros, invoked directly
by the ClojureScript compiler!). The flow-control macros do not behave
exactly like functions, because they can cause some of their arguments not
to be evaluated. But well-written macros generally follow the behavior you
expect: for example, ClojureScript’s and
and or
macros are “short-circuiting” just like the Boolean operators in
JavaScript.
The first answer to “When should I write a macro?” is usually “Don’t!” Macros are the most powerful feature of a Lisp-like language, and the easiest to misuse. In general, you should always use functions and values as the primary units of abstraction in your code. Typically you only need macros in three cases:
To do things functions cannot do. For example, the
and
conditional operation cannot be written as a
function, because it needs to prevent evaluation of some of its
arguments. Macros can control when and how their arguments are
evaluated.[3]
To add a layer of syntactic sugar. For example, the
when
macro doesn’t do anything different from what you
can already accomplish with if
and do
, but
it makes the syntax shorter and easier to read.
To improve performance. Because macros are evaluated during compilation, they can potentially convert an expression into a more-efficient form before it reaches the compiler. The ClojureScript compiler uses macros internally to produce more efficient code, but you are unlikely to encounter this situation in everyday programming.
Macros are an extremely powerful language tool, so powerful that
they are rarely needed in everyday programming. However, for advanced
tasks, such as defining new control structures or embedding
domain-specific languages, they can be invaluable. This chapter has barely
scratched the surface of what macros can do. For more examples, refer to
books about Clojure. For even deeper exploration of macros, look to books
on Common Lisp, such as Paul Graham’s classic On
Lisp, available free online. Note that most other Lisps
use the comma character instead of tilde for unquote
.
[3] Technically, you can prevent evaluation of function arguments by wrapping each argument in an anonymous function, but this is syntactically cumbersome.