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)
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 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].
(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))))
(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"
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");
}
(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
` 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")
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))
(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"))
(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)
... (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__)))
(defn my-logger
[foo]
(log/info foo))
(defmacro my-logger
[foo]
`(log/info ~foo))
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))
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