1. Writing Macros
Chapter 8 of CLOJURE for the BRAVE and TRUE
( LINK : http://www.braveclojure.com/writing-macros/ )
2016/8/11 - ClojureTW 讀書會 @ 摩茲工寮, 台北
2. Macros Are Essential
● Macros are an integral part of Clojure development
— they’re even used to provide fundamental operations.
● EXAMPLE: “ when ” = “ if ” + “ do ”
( macroexpand ‘( when boolean-expression
expression-1
expression-2
expression-3))
; => (if boolean-expression
; (do expression-1
; expression-2
; expression-3))
3. Anatomy of a Macro
Macros receive unevaluated, arbitrary data structures as arguments and
return data structures that Clojure evaluates
defmacro defn
(evaluated)
4. Building Lists for Evaluation
● Macro writing is all about building a list for Clojure to evaluate
defmacro final list to evaluate
unevaluated arguments
5. Building Lists for Evaluation
● Macro writing is all about building a list for Clojure to evaluate
(defmacro infix-2
[[operand1 op operand2]]
(list op operand1 operand2))
6. Building Lists for Evaluation
● You’ll need to be extra careful about the difference
between a symbol and its value
Macro
(building lists)
closed-box
(list of symbol)
Evaluation
7. Building Lists for Evaluation
● You’ll need to be extra careful about the difference
between a symbol and its value
(defmacro my-print-whoopsie
[expression]
(list let [result expression]
(list println result)
result))
EXCEPTION
8. Building Lists for Evaluation
● You’ll need to be extra careful about the difference
between a symbol and its value
(defmacro my-print-whoopsie
[expression]
(list 'let ['result expression]
(list 'println 'result)
result))
OK!
9. Building Lists for Evaluation
Single Quoting vs. Syntax Quoting
'+
; => +
`+
; => clojure.core/+
Syntax quoting will always include the symbol’s full namespace
→ Help you avoid name collisions
10. Building Lists for Evaluation
Single Quoting vs. Syntax Quoting
'(+ 1 ~(inc 1))
; => (+ 1 ~(inc 1))
`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)
full namespace evaluated instead of being quoted!
11. Building Lists for Evaluation
Single Quoting vs. Syntax Quoting
The other difference between quoting and syntax quoting is that the latter
allows you to unquote forms using the tilde, ~
( q u o t e d )
‘
( q u o t e d
` ~ ( evaluated ) q u o t e d )
12. Using Syntax Quoting in a Macro
If you want your macro to return multiple forms for Clojure to evaluate,
make sure to wrap them in a do.
(defmacro code-critic
"Phrases are courtesy Hermes Conrad from Futurama"
[bad good]
`(do (println "Great squid of Madrid, this is bad code:"
(quote ~bad))
(println "Sweet gorilla of Manila, this is good code:"
(quote ~good))))
13. Refactoring a Macro and Unquote Splicing
(do
((clojure.core/println "criticism" '(1 + 1))
(clojure.core/println "criticism" '(+ 1 1))))
(do
(nil nil))
EXCEPTION
NullPointerException
nil function nil argument
Unquote splicing was invented precisely to handle this kind of situation
14. Refactoring a Macro and Unquote Splicing
`(+ ~(list 1 2 3))
; => (clojure.core/+ (1 2 3))
`(+ ~@(list 1 2 3))
; => (clojure.core/+ 1 2 3) Great !
(defmacro code-critic
[{:keys [good bad]}]
`(do ~@(map #(apply criticize-code %)
[["Sweet lion of Zion, this is bad code:" bad]
["Great cow of Moscow, this is good code:"
good]])))
do & map
so convenient !
15. Refactoring a Macro and Unquote Splicing
( q u o t e d
` ~ ( evaluated ) q u o t e d )
( q u o t e d )
‘
( q u o t e d
` ~ ( evaluated ) q u o t e d )@
Unquote
simple function
simple function
+ do, map … etc.
16. Things to Watch Out For
1. Variable Capture
(def message "Good job!")
(defmacro with-mischief
[& stuff-to-do]
(concat (list 'let ['message "Oh, big deal!"])
stuff-to-do))
(with-mischief
(println "Here's how I feel about that thing you did: " message))
; => Here's how I feel about that thing you did:Oh, big deal!
17. Things to Watch Out For
1. Variable Capture
Uncorrelated Macro
letB -> C
function
( input: A)
( output: A)
function
( input: A)
( B ->C)
( output: A)
function
( input: B)
( B ->C)
( output: C)
B
B
B
C
18. Things to Watch Out For
1. Variable Capture
(def message "Good job!")
(defmacro with-mischief
[& stuff-to-do]
`(let [message "Oh, big deal!" ]
~@stuff-to-do))
(with-mischief
(println "Here's how I feel about that thing you did: " message))
; Exception: Can't let qualified name: user/message
Syntax quoting is designed to prevent you from accidentally capturing
variables within macros.
19. Things to Watch Out For
1. Variable Capture
(defmacro without-mischief
[& stuff-to-do]
(let [macro-message (gensym 'message)]
`(let [~macro-message "Oh, big deal!" ]
~@stuff-to-do
(println "I still need to say: " ~macro-message))))
(without-mischief
(println "Here's how I feel about that thing you did: " message))
; => Here's how I feel about that thing you did: Good job!
(gensym 'message)
; => message4763
Symbol prefix !
This example avoids variable capture by using gensym to create a new, unique
symbol that then gets bound to macro-variable.
20. Things to Watch Out For
1. Variable Capture
Uncorrelated Macro
gensym let B -> C
function
( input: A)
( output: A)
function
( input: A)
( gensym B ->C)
( output: A)
function
( input: B)
( B123 ->C)
( output: B)
B
B
B
B
21. Things to Watch Out For
1. Variable Capture
`(blarg# blarg#)
(blarg__2869__auto__ blarg__2869__auto__)
`(let [name# "Larry Potter" ] name#)
; => (clojure.core/let [name__2872__auto__ "Larry Potter"]
name__2872__auto__)
Because this is such a common pattern, you can use an auto-gensym.
Auto-gensyms are more concise and convenient ways to use gensyms
22. Things to Watch Out For
( q u o t e d
` ~ ( evaluated ) q u o t e d )
( q u o t e d
` ~ ( evaluated ) q u o t e d )@
Unquote
+ do, map … etc.
( quoted
` ~ ( evaluated )( let [ var # … ]
autogem
23. Things to Watch Out For
2. Double Evaluation
(defmacro report
[to-try]
`(if ~to-try
(println (quote ~to-try) "was successful:" ~to-try)
(println (quote ~to-try) "was not successful:" ~to-try)))
1
2
3
24. Things to Watch Out For
2. Double Evaluation
Repeated actions in Macro
let repeat 100 times
function
( input: A)
a lot of calc
( output: A)
function
( input: A)
a lot of
repeating calc
( output: A)
B
B
B
function
( input: A)
a lot of
repeating calc
( output: A)
Waiting for
calculation process ….
25. Things to Watch Out For
2. Double Evaluation
(defmacro report
[to-try]
`(if ~to-try
(println (quote ~to-try) "was successful:" ~to-try)
(println (quote ~to-try) "was not successful:" ~to-try)))
1
2
3
(defmacro report
[to-try]
`(let [result# ~to-try]
(if result#
(println (quote ~to-try) "was successful:" result#)
(println (quote ~to-try) "was not successful:" result#))))
1 Double eval ? → let
→ Var capture ? → auto-gensym
26. Things to Watch Out For
3. Macros All the Way Down
(doseq [code ['(= 1 1) '(= 1 2)]]
(report code))
; => code was successful: (= 1 1)
; => code was successful: (= 1 2)
(defmacro doseq-macro
[macroname & args]
`(do
~@(map (fn [arg] (list macroname arg)) args)))
(doseq-macro report (= 1 1) (= 1 2))
; => (= 1 1) was successful: true
; => (= 1 2) was not successful: false
report operating at macro expansion
time, just can’t access those values.
To resolve this situation,
you might write another macro
27. Things to Watch Out For
3. Macros All the Way Down
● It’s easy to paint yourself into a corner, making it impossible to
accomplish anything with run-of-the-mill function calls.
● They only really compose with each other, so by using them, you might be
missing out on the other kinds of composition (functional,
object-oriented) available to you in Clojure.
Macro Macro
Macro
Macro
Macro
Macro
Macro
Macro
Macro
Macro
Macro
Macro
28. Summary
Don’t listen to these prudes
— at least, not at first! Go out there and have a good time.
That’s the only way you’ll learn the situations where it’s appropriate
to use macros. You’ll come out the other side knowing how to use
macros with skill and panache.