Clojure Macro Cheat Sheet
If you want to learn how to write Clojure macros, I recommend reading the macros chapter of Clojure for the Brave and True.
But for when you just want to remember the key parts of writing Clojure macros, here’s a handy cheat sheet.
Table of Contents
Quick Summary
- Syntax Quote:
`
(grave accent)- Usage: prefix to form
- Purpose: Start a quoted form; kinda like starting a code “template”
- Unquote:
~
(tilde)- Usage: prefix to form
- Purpose: Within a syntax-quoted form, stop quoting, evaluate code instead
- Splicing unquote:
~@
- Usage: prefix to form
- Purpose: Unquote which also unwraps a sequence returned from evaluation
- Auto-gensym:
#
(hash, pound)- Usage: postfix to symbol
- Purpose: Use on all local symbols created inside a syntax quoted form; prevents variable capture
- Gensym:
gensym
- Usage: Fn; call on quoted symbol
- Purpose: Use for all local symbols you want to create when syntax quoting is not currently in effect
- Macro expansion:
macroexpand-1
- Usage: Fn; call on quoted form
- Purpose: Debugging; view effect of macro on a (quoted) form
Tool | Symbol | Usage | Purpose |
---|---|---|---|
Syntax quote | ` (grave accent) | prefix to form | Start a quoted form; kinda like starting a code “template” |
Unquote | ~ (tilde) | prefix to form | Within a syntax-quoted form, stop quoting, evaluate code instead |
Splicing unquote | ~@ | prefix to form | Unquote which also unwraps a sequence returned from evaluation |
Auto-gensym | # (hash, pound) | postfix to symbol | Use on all local symbols created inside a syntax quoted form; prevents variable capture |
Gensym | gensym | Fn; call on quoted symbol | Use for all local symbols you want to create when syntax quoting is not currently in effect |
Macro expansion | macroexpand-1 | Fn; call on quoted form | Debugging; view effect of macro on a (quoted) form |
Most of these in an example:
(defmacro ignore-ex [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex#
(printf
"I don't care! Exception: %s\n"
caught-ex#))))
(ignore-ex ArithmeticException
(/ 1 0))
;;=> Expands to the equivalent of
(try
(/ 1 0)
(catch ArithmeticException caught-ex
(printf
"I don't care! Exception: %s"
caught-ex)))
(defmacro ignore-ex [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex#
(printf "I don't care! Exception: %s\n"
caught-ex#))))
(ignore-ex ArithmeticException
(/ 1 0))
;;=> Expands to the equivalent of
(try
(/ 1 0)
(catch ArithmeticException caught-ex
(printf "I don't care! Exception: %s" caught-ex)))
Tools Overview
Debugging: macroexpand-1
& eval
The main tool of your trade for debugging macros is the macroexpand-1
function.
Remember that you have to pass it a quoted form:
(macroexpand-1
'(when true
(println "foo")
(println "bar")))
;;=>
(if true
(do (println "foo")
(println "bar")))
This returns an unevaluated form; use eval
to evaluate it:
(eval *1)
;;=>
foo
bar
Quoting & unquoting
Create unevaluated forms with `
, which you can inject evaluated forms into using ~
.
(defmacro nums-to-add []
`(+ 1 ~(inc 3)))
(macroexpand-1 '(nums-to-add))
;; =>
(clojure.core/+ 1 4)
(eval *1) ;=> 5
Splicing Unquote
Use ~@
to unquote a form which, when evaluated, becomes a sequence that you
then want to unwrap.
The most common use-case for this is including an arbitrary number of “body” forms:
(defmacro dooo [& body]
`(do (do (do ~@body))))
(macroexpand-1 '(dooo (println "foo")
(println "bar")))
;;=>
(do (do (do (println "foo")
(println "bar"))))
(eval *1)
;;=>
foo
bar
Forgetting to splice
Here’s what happens when you forget to use ~@
, and just do a normal unquote
~
instead:
(defmacro dooo-no-splice [& body]
;; NB only using ~ instead of ~@ here!
`(do (do (do ~body))))
(macroexpand-1
'(dooo-no-splice (println "foo")
(println "bar")))
;;=> NB note the unwanted
;; extra wrapping parens!
(do (do (do ((println "foo")
(println "bar")))))
(eval *1)
;;=>
foo
bar
Execution error (NullPointerException)
at scratch.core/eval23583 (REPL:326).
Cannot invoke
"clojure.lang.IFn.invoke(Object)"
because the return value of
"clojure.lang.IFn.invoke(Object)"
is null
(defmacro dooo-no-splice [& body]
;; NB only using ~ instead of ~@ here!
`(do (do (do ~body))))
(macroexpand-1
'(dooo-no-splice (println "foo")
(println "bar")))
;;=> NB note the unwanted extra wrapping parens!
(do (do (do ((println "foo")
(println "bar")))))
(eval *1)
;;=>
foo
bar
Execution error (NullPointerException) at scratch.core/eval23583 (REPL:326).
Cannot invoke "clojure.lang.IFn.invoke(Object)" because the return value of
"clojure.lang.IFn.invoke(Object)" is null
Auto-gensym
Whenever you need to create a local variable binding within syntax-quoted code, you need to gensym it:
(defmacro ignore-ex [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex#
(printf
"I don't care! Exception: %s\n"
caught-ex#))))
(macroexpand-1
'(ignore-ex ArithmeticException
(/ 1 0)))
;;=>
(try
(/ 1 0)
(catch ArithmeticException
caught-ex__23712__auto__
(clojure.core/printf
"I don't care! Exception: %s\n"
caught-ex__23712__auto__)))
(eval *1)
;;=>
; I don't care! Exception:
; java.lang.ArithmeticException:
; Divide by zero
(defmacro ignore-ex [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex#
(printf "I don't care! Exception: %s\n"
caught-ex#))))
(macroexpand-1
'(ignore-ex ArithmeticException
(/ 1 0)))
;;=>
(try
(/ 1 0)
(catch ArithmeticException caught-ex__23712__auto__
(clojure.core/printf "I don't care! Exception: %s\n"
caught-ex__23712__auto__)))
(eval *1)
;;=>
; I don't care! Exception: java.lang.ArithmeticException: Divide by zero
Forgetting to use auto-gensym
If you forget to use a gensym, you’ll generally get some sort of compilation error. This is a good thing though, because it’s better than having unnoticed variable capture.
(defmacro ignore-ex-no-gensym [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex
(printf
"I don't care! Exception: %s\n"
caught-ex))))
(ignore-ex-no-gensym ArithmeticException
(/ 1 0))
;;=>
; Syntax error compiling try
; Can't bind qualified name:
; scratch.core/caught-ex
(defmacro ignore-ex-no-gensym [ex-class & body]
`(try
~@body
(catch ~ex-class caught-ex
(printf
"I don't care! Exception: %s\n"
caught-ex))))
(ignore-ex-no-gensym ArithmeticException
(/ 1 0))
;;=>
; Syntax error compiling try
; Can't bind qualified name:scratch.core/caught-ex
Manual Gensym
When you need to generate a symbol within an unquoted expression, you need to
call the gensym
explicitly:
(defmacro with-log-context
"Set SLF4J MC logging context
key-value pairs, clearing them
from the logging context when
the form exits."
[bindings & body]
`(with-open
~(->> (partition 2 bindings)
(mapcat
(fn [[k v]]
[(gensym '_)
`(org.slf4j.MDC/putCloseable
~k
(str ~v))]))
vec)
~@body))
(macroexpand-1
'(with-log-context ["foo" "bar"
"baz" "qux"]
(log/info "hello!")))
;;=>
(clojure.core/with-open
[_23618
(org.slf4j.MDC/putCloseable
"foo"
(clojure.core/str "bar"))
_23619
(org.slf4j.MDC/putCloseable
"baz"
(clojure.core/str "qux"))]
(log/info "hello!"))
;; i.e.
(with-open
[_x (MDC/putCloseable "foo"
(str "bar"))
_y (MDC/putCloseable "baz"
(str "qux"))]
(log/info "hello!"))
(defmacro with-log-context
"Set SLF4J MC logging context key-value pairs, clearing them from the logging
context when the form exits."
[bindings & body]
`(with-open
~(->> (partition 2 bindings)
(mapcat (fn [[k v]]
[(gensym '_) `(org.slf4j.MDC/putCloseable ~k (str ~v))]))
vec)
~@body))
(macroexpand-1
'(with-log-context ["foo" "bar"
"baz" "qux"]
(log/info "hello!")))
;;=>
(clojure.core/with-open
[_23618
(org.slf4j.MDC/putCloseable "foo" (clojure.core/str "bar"))
_23619
(org.slf4j.MDC/putCloseable "baz" (clojure.core/str "qux"))]
(log/info "hello!"))
;; i.e.
(with-open [_x (MDC/putCloseable "foo" (str "bar"))
_y (MDC/putCloseable "baz" (str "qux"))]
(log/info "hello!"))
Note also in the above example that we’re using nested syntax quoting. (Insert Inception reference here…)
Further Macro Examples
Other examples for inspiration and to help show the above tools in context:
Succinct Stubbing
You might want to create a succinct way of stubbing very commonly redefined functions in your tests:
(defn widget-fetch-fn []
:foo)
(defmacro stub-widget
[widget & body]
`(with-redefs
[widget-fetch-fn (constantly ~widget)]
~@body))
(stub-widget :bar
(widget-fetch-fn))
;;=> :bar
(macroexpand-1
'(stub-widget :bar
(widget-fetch-fn)))
;;=>
(clojure.core/with-redefs
[scratch.core/widget-fetch-fn
(clojure.core/constantly :bar)]
(widget-fetch-fn))
;; i.e.
(with-redefs
[widget-fetch-fn (constantly :bar)]
(widget-fetch-fn))
(defn widget-fetch-fn []
:foo)
(defmacro stub-widget [widget & body]
`(with-redefs [widget-fetch-fn (constantly ~widget)]
~@body))
(stub-widget :bar
(widget-fetch-fn))
;;=> :bar
(macroexpand-1
'(stub-widget :bar
(widget-fetch-fn)))
;;=>
(clojure.core/with-redefs
[scratch.core/widget-fetch-fn (clojure.core/constantly :bar)]
(widget-fetch-fn))
;; i.e.
(with-redefs [widget-fetch-fn (constantly :bar)]
(widget-fetch-fn))
Testing that a spec exception has been thrown
(defmacro fails-with-spec-error
[& body]
`(try
~@body
(is false
(str "Shouldn't get here; "
"expecting an exception"))
(catch clojure.lang.ExceptionInfo ex#
(is (= :instrument
(:clojure.spec.alpha/failure
(ex-data ex#)))))))
(macroexpand-1
'(fails-with-spec-error
(fn-which-should-trigger-error)))
;;=>
(try
(fn-which-should-trigger-error)
(clojure.test/is
false
(clojure.core/str
"Shouldn't get here; "
"expecting an exception"))
(catch
clojure.lang.ExceptionInfo
ex__23627__auto__
(clojure.test/is
(clojure.core/=
:instrument
(:clojure.spec.alpha/failure
(clojure.core/ex-data
ex__23627__auto__))))))
;; i.e.
(try
(fn-which-should-trigger-error)
(is false
(str "Shouldn't get here; "
"expecting an exception"))
(catch clojure.lang.ExceptionInfo ex
(is (= :instrument
(:clojure.spec.alpha/failure
(ex-data ex))))))
(defmacro fails-with-spec-error [& body]
`(try
~@body
(is false "Shouldn't get here; expecting an exception")
(catch clojure.lang.ExceptionInfo ex#
(is (= :instrument
(:clojure.spec.alpha/failure (ex-data ex#)))))))
(macroexpand-1
'(fails-with-spec-error
(fn-which-should-trigger-error)))
;;=>
(try
(fn-which-should-trigger-error)
(clojure.test/is false "Shouldn't get here; expecting an exception")
(catch clojure.lang.ExceptionInfo ex__21714__auto__
(clojure.test/is
(clojure.core/=
:instrument
(:clojure.spec.alpha/failure
(clojure.core/ex-data ex__21714__auto__))))))
;; i.e.
(try
(fn-which-should-trigger-error)
(is false "Shouldn't get here; expecting an exception")
(catch clojure.lang.ExceptionInfo ex
(is (= :instrument
(:clojure.spec.alpha/failure (ex-data ex))))))