Clojure
Macro Writing 101
Before we start...
In clojure we often think with vectors as data structures, but we have lists too!
'(:a :b)
'(:a (:d (:e :f)))
We can nest these too!
:a
\
.
/ \
:d nil
\
.
/ \
. nil
/ \
:e .
/ \
:f nil
This is actually a flat representation of a tree (s-exps)
This looks kinda like...
Or rather *exactly* like our code
'(:a (:b :c) :d :e)
'(if (neg? x) "negative" "positive")
(second '(if (neg? x) "negative" "positive"))
=> (neg? x)
(defn replace-third-with-neg
[[a b c d]]
(cons a (cons b (cons "neg" (cons d nil)))))
(replace-third-with-neg
'(if (neg? x) "negative" "positive"))
=> (if (neg? x) "neg" "positive")
(defn way-easier?
[[a b c d]]
'(a b "neg" d))
(way-easier?
'(if (neg? x) "negative" "positive"))
=> (a b "neg" d)
Fancy word "homoiconic"
Macros!
Macros give us the ability to manipulate data easily,
and hence our code easily.
When you think about Macros, as opposed to functions, think about your output being data.
Macros run at compile time, this is a big "gotcha". They *run* and have the full power of Clojure, but they output data (aka "expand") and that data is evaluated at runtime [this can be confusing in the repl].
First Use: Access unevaluated code
(loop [x 1] (prn x) (recur (inc x)))
(defn print-foo [arg] (prn "foo"))
(print-foo (loop [x 1] (prn x) (recur (inc x))))
(defmacro macro-foo
[should-run arg]
(prn "foo")
(when should-run arg))
(defn -main [& args]
(prn "Starting")
(macro-foo false (loop [x 1] (prn x) (recur (inc x))))
(prn "Done"))
(when should-run (loop [x 1] (prn x) (recur (inc x))))
Wait what happened?
(defmacro macro-foo
[should-run arg]
(prn "foo")
(when should-run arg))
(macro-foo false (loop [x 1] (prn x) (recur (inc x))))
macrowriting-101.core=> (macroexpand '(macro-foo false
(loop [x 1] (prn x) (recur (inc x)))))
"foo"
nil
macrowriting-101.core=> (macroexpand '(macro-foo true
(loop [x 1] (prn x) (recur (inc x)))))
"foo"
(loop* [x 1] (prn x) (recur (inc x)))
$ java -jar target/uberjar/macrowriting-101-0.1.0-SNAPSHOT-standalone.jar
"Starting"
"Done"
Wait what happened?
public final class core$_main extends RestFn
{
public static final Var const__0 = (Var)RT.var("clojure.core", "prn");
public Object doInvoke(Object args) {
((IFn)const__0.getRawRoot()).invoke("Starting"); null;
return ((IFn)const__0.getRawRoot()).invoke("Done");
}
We haven't gone far enough yet.
(defmacro macro-foo
[should-run arg]
(prn "foo")
(when should-run arg))
(def should-run false)
(macro-foo should-run (loop [x 1] (prn x) (recur (inc x))))
When do you use this? (hint: every day)
(defmacro macro-foo
[should-run arg]
`(when ~should-run ~arg))
(def should-run false)
(macro-foo should-run (loop [x 1] (prn x) (recur (inc x))))
user=> (macro-foo should-run (loop [x 1] (prn x) (recur (inc x))))
nil
Our new syntax friends
` is called "syntax-quote"
~ is called "unquote"
=> `(should-run)
(user/should-run)
=> `(~should-run)
(false)
=> '(~should-run)
((clojure.core/unquote should-run))
(defn way-easier?
[[a b c d]]
`(~a ~b "neg" ~d))
(way-easier?
'(if (neg? x) "negative" "positive"))
=> (if (neg? x) "neg" "positive")
Second Use: Compile Time Work
Back to the (prn "foo") we have the full power of Clojure at compile time...
(defmacro welcome-message
[]
(let [version-number (slurp "./version-file")
log-message (format
"Welcome to Foo, running on version %s"
version-number)]
`(log/info ~log-message))
(defn startup [] (welcome-message))
Third Use: Create your own Syntax
(defn log-throws
[user-fn]
(try
(user-fn)
(catch Exception ex
(log/error ex "Badness")
(throw ex))))
(log-throws #(do
(prn "a")
(prn "b")))
(defmacro log-throws
[& body]
`(try
~@body
(catch Exception ex#
(log/error ex# "Badness")
(throw ex#))))
(log-throws
(prn "a")
(prn "b"))
Two new syntax bits
(defmacro log-throws
[& body]
`(try
~@body
(catch Exception ex#
(log/error ex# "Badness")
(throw ex#))))
~@ is called "unquote-splice", it's used with seqs, example from clojure docs
user=> (let [x `(2 3)]
`(1 ~x))
(1 (2 3))
user=> (let [x `(2 3)]
`(1 ~@x))
(1 2 3)
Two new syntax bits
... (catch Exception ex# ...
# is gensym, to generate a symbol,
if we don't gensym, then the macro expands to look in the surrounding scope.
(defmacro log-throws
[& body]
`(try
~@body
(catch Exception ex
(log/error ex "Badness")
(throw ex))))
(macroexpand '(log-throws (prn "a")))
(try (prn "a") (catch java.lang.Exception user.core/ex (clojure.tools.logging/error
user.core/ex "Badness") (throw user.core/ex)))
;; with gensym
(try (prn "a") (catch java.lang.Exception ex__1898__auto__
(clojure.tools.logging/error ex__1898__auto__ "Badness") (throw ex__1898__auto__)))
Fourth use: Inline code
(defn my-logger
[foo]
(log/info foo))
(defmacro my-logger
[foo]
`(log/info ~foo))
Macro Limitations
Macros aren't functions. They can't be passed as functions or composed
(apply or '(true true false))
(reduce or '(true true false))
CompilerException java.lang.RuntimeException: Can't take value
of a macro: #'clojure.core/or, compiling:
(/private/var/folders/z3/_y00v1fn0zj7d/T/form-init49956851841.clj:1:1)
Macros are tricky to test.
Macros are tricky to debug.
Macros can surprise your fellow engineers.
(infix
(2 + 3))
Macro Cheatsheet
macroexpand - see what data a macro outputs (macroexpand '(foo true))
' - quote, treat next form as data '(:a :b :c)
` - syntax-quote, fully qualifies symbol names within, allows for unquoting `(foo bar)
~ - unquote, resolves a symbol within a syntax-quote `(foo ~bar)
~@ - unquote-splice, inserts the contents of a sequence at that point `(try ~@body (catch ...))
# - added to the end of a symbol will generate a symbol in that scope `(let [x# 5] (prn x#))
1. Access unevaled args
2. Compile time work
3. Create new syntax
4. Inline code
Clojure Macro Writing 101
By Philip Doctor
Clojure Macro Writing 101
- 1,558