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
Recomputing Values
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 println
expression.
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.
Variable Capture
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 #
variable
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.
Writing Macros
So how do we go about writing macros that are correct?
Input Examples
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 debug
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.
Output Examples
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#))
(The 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.
Debugging
Of course, occasionally you might make a mistake. You can see what expression
a macro produces using the macroexpand-1
function:
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
is.
In the next posting, we’ll create a macro to use in place of the ends?
function.
2 comments:
I think this is the simplest macro tutorial I've ever read. Thanks for this.
Glad you found it helpful!
Post a Comment