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