Monday, July 21, 2008

Stemming, Part 9: Writing Macros

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
     (println '~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

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!

“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
  `(let [value ~expr]
     (println '~expr "=>" 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
  `(let [value# ~expr]
     (println '~expr "=>" value#)

Let’s try it out.

user=> (debug (println "Count Me!"))
Count Me!
(println Count Me!) => 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#)

;(debug (+ 1 2))
(let [value# (+ 1 2)]
  (println '(+ 1 2) "=>" value#)

;(debug (:name person))
(let [value# (:name person)]
  (println '(:name person) "=>" 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
  `(let [value# ~expr]
     (println '~expr "=>" 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.


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)

(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.


Efe Ariaroo said...

I think this is the simplest macro tutorial I've ever read. Thanks for this.

Eric Rochester said...

Glad you found it helpful!