In the last posting, I introduced macros. Today, I’m going to point out some of the pitfalls in writing macros and suggest a method for writing them that help you avoid those potential problems.
To illustrate these issues, let’s take a look at a simplified, incorrect
version of the
debug macro from the last posting:
(defmacro debug [expr] `(do (println '~expr "=>" ~expr) ~expr))
Superficially, this version appears to work correctly. At least, if you try it out, it seemed to work:
user=> (debug (+ 1 2)) (+ 1 2) => 3 3
The first problem with
debug as given above can best be illustrated by
passing it an expression that has a side-effect, such as a
user=> (debug (println "Count Me!")) Count Me! (println Count Me!) => nil Count Me! nil
“Count Me!” gets printed twice. That’s because the expression’s getting executed twice: once when its value is printed and once when its value is being returned. In this case, that might be fine, but imagine if the value took a long time to compute or if it deleted a file or inserted a new value into the database. The macro would take twice as long to run; it would cause an error when it tried again to delete a file it had just deleted; or it would insert a value into the database twice. None of those are good options.
Instead of having the value computed twice, we need it to be computed only
once. We can get that by added a
let to the macro that computes the
expression’s value once and stores it in a variable.
(defmacro debug [expr] `(let [value ~expr] (println '~expr "=>" value) value))
Now if we try it again:
user=> (debug (println "Count Me!")) java.lang.Exception: Can't let qualified name: user/value
Hmm. That introduces the second type of error.
Basically, Clojure was complaining because macros return symbols that are
attached to a namespace (
user, in this case). But variables have to exist
outside of any namespace. To fix it, we change
value to use the
notation I mentioned in the last posting.
(defmacro debug [expr] `(let [value# ~expr] (println '~expr "=>" value#) value#))
Let’s try it out.
user=> (debug (println "Count Me!")) Count Me! (println Count Me!) => nil nil
There. That fixed both problems. Now it only evaluates the expression once, and it won’t clobber any variables from the surrounding context. Now it’s correct.
So how do we go about writing macros that are correct?
First, create several examples of what you want the input to look like. For the debug macro, that might look like:
(debug name) (debug (+ 1 2)) (debug (:name person))
Make sure to include many different types of input parameters. In the
macro, the first problem—recomputing values—won’t appear if you always pass it
a variable or another expression that doesn’t require computation. This is
just good development and testing: test thoroughly.
Next, for each input expression, write what you want the corresponding output expression to be:
;(debug name) (let [n# name] (println 'name "=>" n#) n#) ;(debug (+ 1 2)) (let [value# (+ 1 2)] (println '(+ 1 2) "=>" value#) value#) ;(debug (:name person)) (let [value# (:name person)] (println '(:name person) "=>" value#) value#)
Write the Macro
Now that we have the pairs of input and output, we can write a macro that converts the first expression into the second. The result is what we had in the last posting:
(defmacro debug [expr] `(let [value# ~expr] (println '~expr "=>" value#) (flush) value#))
flush function just makes sure that the expression is written out
immediately. Otherwise, the computer might sit on it for a while.)
As you write the macro, think about the two common macro mistakes I outlined above.
Of course, occasionally you might make a mistake. You can see what expression
a macro produces using the
user=> (macroexpand-1 '(debug (+ 1 2))) (clojure/let [value__3063 (+ 1 2)] (clojure/println (quote (+ 1 2)) "=>" value__3063) (clojure/flush) value__3063)
(I’ve broken the lines up to make them more readable.)
Examining the output of
macroexpand-1 should make it clear where the problem
In the next posting, we’ll create a macro to use in place of the