At Swym, we use Clojure for a lot of things. And invariably, we have ended up with use cases where macros are deemed necessary. In this post, I try to take through the lessons I learned (read mistakes) along the way.
I started off by creating a simple macro using the macro _defmacro_
.
Note — The code output is commented out so anyone can copy paste to a repl without issues, but the syntax highlight doesn’t show it as commented out.
(defmacro not-really-macro [a] (do (println a) a));#'user/not-really-macro
(macroexpand `(not-really-macro "test"));test;"test"
(not-really-macro "test");test;"test"
_not-really-macro_
is just a println code that will execute as soon you call it. So not really a macro. Moving on
Lesson 0 — Not everything defined with **_defmacro_**
qualifies as a macro.
;; without list, using syntax quoting(defmacro wrong-macro [a] `(do (println a) a))
(macroexpand `(wrong-macro "test"));(do (clojure.core/println user/a) user/a)
(wrong-macro "test");CompilerException java.lang.RuntimeException: No such var: user/a, ...
May have been a macro if it worked. The error is clear enough, there is no a defined in the user namespace. Which is weird as I expect that to come from the input arg to the macro, right? Hmm, what if I switch the namespace and try expanding
(ns outerspace);nil;in outerspace now
(macroexpand `(user/wrong-macro "test"));(do (clojure.core/println user/a) user/a)
Now, that is interesting. The namespace of _a_
didn’t change. So the macro expansion is not picking up the arg passed.
Since I quoted the code, I need to _unquote_
to access the symbols outside the _quote_
.
(in-ns 'user)(defmacro ok-macro [a] `(do (println ~a) ~a));#'user/ok-macro
(macroexpand `(ok-macro "test"));(do (clojure.core/println "test") "test")
(ok-macro "test");"test";test
A double WooHoo! moment, both the macro expansion and the actual output was correct.
Lesson 1 — When in quoted code, access outside references using unquote.
So, there are clojure functions _quote_
, _unquote_
and _unquote-splicing_
. Trying to use them in the macro,
(defmacro forcequote-macro [a] (quote (do (println ~a) ~a)));#'user/forcequote-macro
(macroexpand `(forcequote-macro "test"));(do (println (clojure.core/unquote a)) (clojure.core/unquote a))
(forcequote-macro "test");CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context...
Huh? Why not?
Lesson 2 — ` is not the literal shortcut to quote.
To confirm that lesson 2 is actually true,
(= 'a (quote a));true
(= `a (quote a));false
Success! Sort of.
Lesson 3 — ‘ is the literal shortcut to quote.
Now, doing the equivalent for _unquote_
.
(defmacro forceunquote-macro [a] `(do (println (unquote a)) (unquote a)));#'user/forceunquote-macro
(macroexpand `(forceunquote-macro "test"));(do (clojure.core/println (clojure.core/unquote user/a)) (clojure.core/unquote user/a))
(forceunquote-macro "test");CompilerException java.lang.RuntimeException: No such var: user/a, compiling...
Hmm, there seems to be a pattern to this “madness” (or so I called it).
Lesson 4 — ~ is not the literal shortcut to unquote.
So, the unquoting and quoting were not working when called by reference, meaning it was too late to identify the symbols that are used inside the macro. Literals had to be used, no two ways about it.
If the problem is the symbol, why not just generate a symbol I needed on demand inside that macro and referenced that instead. Here it goes
;; gensym(defmacro sym-gen-macro [a](let [dyn-a (gensym a)]`(let [~dyn-a ~a](println ~dyn-a)~dyn-a)));#'user/sym-gen-macro
(macroexpand `(sym-gen-macro "test"));(let* [test1663 "test"] (clojure.core/println test1663) test1663)
(sym-gen-macro "test");test;"test"
_gensym_
to the rescue, the macro worked! But that didn’t seem right. It works, but at what cost. Every time the macro was invoked, there is a new symbol created and the reference is updated to a inside the let anyway. So nope, definitely not it.
Lesson 5 — **_gensym_**
cannot solve your problem of unquoting.
After confirming there is no getting away from those mystery literals, moved to unquote-splicing. Very powerful in using entire body of args to be passed
(defmacro expand-body [& body]`(println ~@body));#'user/expand-body
(macroexpand `(expand-body "test1" "test2"));(clojure.core/println "test1" "test2")
(expand-body "test1" "test2");test1 test2;nil
Worked well, getting the hang of it now. Now I tried using the definition given here. It didn’t work without literals as expected, but I was in for a rude but interesting shock.
(source unquote);(def unquote);nil
(source unquote-splicing);(def unquote-splicing);nil
(source macroexpand));(defn macroexpand; "Repeatedly calls macroexpand-1 on form until it no longer; represents a macro form, then returns it. Note neither; macroexpand-1 nor macroexpand expand macros in subforms."; {:added "1.0"; :static true}; [form]; (let [ex (macroexpand-1 form)]; (if (identical? ex form); form; (macroexpand ex))));nil
(source quote);Source not found;nil
As you can see, unquote and unquote-splicing are _def_
_’_s just symbols, unlike the other _defn_
‘s. So, of course I can’t use them instead of the literals, duh.
Lesson 6 — Not all literals have an equivalent **_defn_**
.
Lesson 7 — Use ~@ to take a list of args expand inside the macro
Extra — Try (source defn)
, it is an interesting read.
I tried adding a new symbol which would prepend to the input string.
(defmacro innersym-macro [a]`(let [dyn-a# (str "Prepend-" ~a)](println dyn-a#)dyn-a#));#'user/innersym-macro
(macroexpand `(innersym-macro "test"));(let* [dyn-a__1749__auto__ (clojure.core/str "Prepend-" "test")] (clojure.core/println dyn-a__1749__auto__) dyn-a__1749__auto__)
(innersym-macro "test");Prepend-test;"Prepend-test"
That is getting close to being awesome!
Lesson 8 — Use # — to create symbols inside the quote-d code block, also known as autogensym.
Pushing my luck, I tried destructuring the inner level args.
(defmacro innerdestructure-macro [a]`(let [{:keys [prepend#] :as aprepender#} {:prepend "Prependtext" :append "Appendtext"}dyn-a# (str prepend# ~a (:append aprepender#))](println dyn-a#)dyn-a#));#'user/innerdestructure-macro
(macroexpand `(innerdestructure-macro "test"));(let* [map__1853 {:prepend "Prependtext", :append "Appendtext"} map__1853 (if (clojure.core/seq? map__1853) (clojure.lang.PersistentHashMap/create (clojure.core/seq map__1853)) map__1853) aprepender__1844__auto__ map__1853 prepend__1843__auto__ (clojure.core/get map__1853 :prepend__1843__auto__) dyn-a__1845__auto__ (clojure.core/str prepend__1843__auto__ "test" (:append aprepender__1844__auto__))] (clojure.core/println dyn-a__1845__auto__) dyn-a__1845__auto__)
(innerdestructure-macro "test");testAppendtext;"testAppendtext"
The destructuring didn’t work, as _prepend#_
got treated as a nil, but the direct get key worked well.
Lesson 9 — Destructuring doesn’t work in the first level of quoted code block
Now, using the awesome macro skills acquired so far, I ventured into creating dynamic symbols inside namespaces whenever a macro is executed.
(defmacro interning-macro [a]`(let [{:keys [prepend#] :as aprepender#} {:prepend "Prependtext" :append "Appendtext"}dyn-a# (str prepend# ~a (:append aprepender#))](intern*ns*'~'ooh-fn(fn [oohargs#](println oohargs# dyn-a#)oohargs#))));#'user/interning-macro
(macroexpand `(interning-macro "test"));(let* [map__1995 {:prepend "Prependtext", :append "Appendtext"} map__1995 (if (clojure.core/seq? map__1995) (clojure.lang.PersistentHashMap/create (clojure.core/seq map__1995)) map__1995) aprepender__1985__auto__ map__1995 prepend__1984__auto__ (clojure.core/get map__1995 :prepend__1984__auto__) dyn-a__1986__auto__ (clojure.core/str prepend__1984__auto__ "test" (:append aprepender__1985__auto__))] (clojure.core/intern clojure.core/*ns* (quote ooh-fn) (clojure.core/fn [oohargs__1987__auto__] (clojure.core/println oohargs__1987__auto__ dyn-a__1986__auto__) oohargs__1987__auto__)))
(ns outerspacestar)(user/interning-macro "star");#'outerspacestar/ooh-fn
(ns outerspaceplanet)(user/interning-macro "planet");#'outerspaceplanet/ooh-fn
(in-ns 'user)(outerspacestar/ooh-fn {:a 10});{:a 10} starAppendtext;{:a 10}
(outerspaceplanet/ooh-fn {:b 20});{:b 20} planetAppendtext;{:b 20}
That was awesome! Having some internal references from when the macro was instantiated. This comes handy in creating repeatable modules with configuration changes. Many lessons in this one
Lesson 10 — ***ns***
— refers to current namespace where the code is executing.
Lesson 11 — **_(intern somens '~'symname <<symdefinition>>)_**
is equivalent to adding **_(def symname symdefinition)_**
in that somens namespace_._
Lesson 12 — **_(def x (fn []))_ = _(defn x [])_**
.
Extra — Checkout Protocols sometime, if you haven’t already i.e.
Now, going for the limit-breaker of my understanding — How about loading a namespace inside the intern of current namespace generated from a macro
(defn resolvable-fn1 [](println "resolved1"));#'user/resolvable-fn1
(defn resolvable-fn2 [](println "resolved2"));#'user/resolvable-fn2
(defmacro interning-resolve-macro [a]`(let [{:keys [prepend#] :as aprepender#} {:prepend "Prependtext" :append "Appendtext"}dyn-a# (str prepend# ~a (:append aprepender#))](intern*ns*'~'resolvens-fn(fn [rargs#]((ns-resolve (symbol "user") (symbol "resolvable-fn1")))((ns-resolve '~'user '~'resolvable-fn2))(println rargs# dyn-a#)rargs#))))#'user/interning-resolve-macro
(macroexpand `(interning-resolve-macro "test"));(let* [map__2276 {:prepend "Prependtext", :append "Appendtext"} map__2276 (if (clojure.core/seq? map__2276) (clojure.lang.PersistentHashMap/create (clojure.core/seq map__2276)) map__2276) aprepender__2266__auto__ map__2276 prepend__2265__auto__ (clojure.core/get map__2276 :prepend__2265__auto__) dyn-a__2267__auto__ (clojure.core/str prepend__2265__auto__ "test" (:append aprepender__2266__auto__))] (clojure.core/intern clojure.core/*ns* (quote resolvens-fn) (clojure.core/fn [rargs__2268__auto__] ((clojure.core/ns-resolve (clojure.core/symbol "user") (clojure.core/symbol "resolvable-fn1"))) ((clojure.core/ns-resolve (quote user) (quote resolvable-fn2))) (clojure.core/println rargs__2268__auto__ dyn-a__2267__auto__) rargs__2268__auto__)))
(ns outerspacestar)(user/interning-resolve-macro "star")#'outerspacestar/resolvens-fn
(ns outerspaceplanet)(user/interning-resolve-macro "planet")#'outerspaceplanet/resolvens-fn
(in-ns 'user)(outerspacestar/resolvens-fn {:a 100});resolved1;resolved2;{:a 100} starAppendtext;{:a 100}
(outerspaceplanet/resolvens-fn {:b 200});resolved1;resolved2;{:b 200} planetAppendtext;{:b 200}
Adding on to aforementioned awesomeness, resolving symbols can be done in interesting ways to successful results.
Lesson 13 — **_(symbol "xyz")_**
= **_'xyz_**
.
Lesson 14 — In macro world — **_(symbol "xyz")_**
= **_'~'xyz_**
.
Lesson 15 — Before a **_ns-resolve_**
is called, the ns needs to have been loaded. So better do a **_(require 'nssymbol)_**
before using **ns-resolve**
.
It was getting a little messy to see all those dynamic symbols in one go. Got me to try out _macroexpand-1_
.
(macroexpand-1 `(interning-resolve-macro "test"))
;(clojure.core/let [{:as aprepender__2266__auto__, :keys [prepend__2265__auto__]} {:prepend "Prependtext", :append "Appendtext"} dyn-a__2267__auto__ (clojure.core/str prepend__2265__auto__ "test" (:append aprepender__2266__auto__))] (clojure.core/intern clojure.core/*ns* (quote resolvens-fn) (clojure.core/fn [rargs__2268__auto__] ((clojure.core/ns-resolve (clojure.core/symbol "user") (clojure.core/symbol "resolvable-fn1"))) ((clojure.core/ns-resolve (quote user) (quote resolvable-fn2))) (clojure.core/println rargs__2268__auto__ dyn-a__2267__auto__) rargs__2268__auto__)))
Neater.
Lesson 16 — **macroexpand-1**
goes 1 level of expansion and macroexpand goes to all levels and expands every. single. macro.
Some good practices that evolved out of the lessons learnt
Macros sure are powerful in many ways, allowing for data to become the code, executable and everything. But (of course there is a “but”) the documentation around it is kind of shrouded in mystery and (for a lack of better word) not simple. Hopefully the lessons from this post help some of those mysteries reveal to the uninitiated.
I am sure I have missed a point or two, so please feel free to correct me wherever necessary in the comments section below. Also, do share your experiences with Clojure macros below, would love to know them!
References and useful links