Macros Can Be Tricky to Get Right

You’ve already seen plenty of subtleties with macros, such as the need to avoid symbol capture with gensyms, and there’s more to come. While you’re in the great position of knowing all the language’s rules for macro construction, you may still be unclear about the implications of what you know. Let’s look at a few more ways you might accidentally cause yourself grief by making the wrong assumptions about macro code.

Here’s a re-implementation of the clojure.core/and macro that seems fairly straightforward.

beware/and_1.clj
 
(​defmacro​ our-and
 
([] true)
 
([x] x)
 
([x & ​next​]
 
`(​if​ ~x (our-and ~@​next​) ~x)))
 
 
user=> (our-and true true)
 
;=> true
 
user=> (our-and true false)
 
;=> false
 
user=> (our-and true true false)
 
;=> false
 
user=> (our-and true true nil)
 
;=> nil
 
user=> (our-and 1 2 3)
 
;=> 3

It appears to have the same behavior as clojure.core/and, returning its argument for the base case of the recursion, returning the first non-truthy value if it fails, and recursing when it doesn’t fail. But what if the caller passes an expression to our-and?

beware/and_2.clj
 
user=> (our-and (​do​ (​println​ ​"hi there"​) (​=​ 1 2)) (​=​ 3 4))
 
;hi there
 
;hi there
 
;=> false

The expression we passed in is actually evaluated twice! Looking back at the macro implementation and a macroexpansion, it’s clear why this happens: because the macroexpansion inserts the expression, in its entirety, in two places:

beware/and_3.clj
 
user=> (​macroexpand-1​ '(our-and (​do​ (​println​ ​"hi there"​) (​=​ 1 2)) (​=​ 3 4)))
 
;=> (if (do (println "hi there") (= 1 2))
 
; (user/our-and (= 3 4))
 
; (do (println "hi there") (= 1 2)))

In our examples, we still get the right answer, but the side effects are troublesome. Imagine what would happen if they were writing to the production database instead of printing log messages for programmers! Of course, ideally we wouldn’t have them, but in practice Clojure programmers do use I/O, atoms, refs, and other side-effecting constructs from time to time.

So we have two choices here: we can either (a) tell clients of our-and to assume that their expression may be evaluated multiple times (via documentation or word of mouth), or (b) fix the macro to evaluate the expressions only once. The choice is clear: it’s always preferable to make macros less surprising when we have the opportunity. Unless we’re building a specialized macro expressly intended to evaluate zero, multiple, or an indeterminate number of times, we should evaluate arguments exactly once. So that’s our default, but part of the power of macros is in being able to choose. The important thing to me as a macro user is that I know what the evaluation semantics are, and I tend to expect arguments to be evaluated once unless I have documentation that says otherwise.

In the case of our-and, we can easily fix it up to evaluate its arguments only once:

beware/and_4.clj
 
(​defmacro​ our-and
 
([] true)
 
([x] x)
 
([x & ​next​]
 
`(​if​ ~x (our-and ~@​next​) ~x)))
 
 
(​defmacro​ our-and-fixed
 
([] true)
 
([x] x)
 
([x & ​next​]
 
`(​let​ [arg# ~x]
 
(​if​ arg# (our-and-fixed ~@​next​) arg#))))
 
 
user=> (our-and-fixed (​do​ (​println​ ​"hi there"​) (​=​ 1 2)) (​=​ 3 4))
 
;hi there
 
;=> false

There’s nothing magical here: extracting a local up to a let binding is the same thing we’d do if we had a duplicated expression inside a function to avoid evaluating it twice.

Proceed with Caution

My point here is only partly that we, as macro authors, should avoid executing input expressions multiple times unless we really, really mean to. But more importantly, it’s that when we write macros, it’s great when we can shield the people who use those macros from having to dig into the macro implementations to see why some surprising thing happens. Our macros are being invited to expand into users’ namespaces, and we should appreciate and respect that invitation by making as little of a mess as possible.

This chapter hasn’t been an exhaustive list of the ways that macros might trip you up. Macroexpand-time errors can sometimes generate confusing compiler errors without significant thought given to error cases. Errors in successfully macroexpanded code leave you with stack traces unaware of the line number in the macro definition that caused the error. Helper functions that need to be called by macroexpanded code must be public (no defn- or ^:private) in order for macro calls to work from other namespaces. We’ve seen other potential stumbling blocks in the last couple of chapters, and we’ll come across more. These are trade-offs we must accept on behalf of our team whenever we decide to use a macro.

But the thing is: sometimes a function won’t do. Although the humble function is a pretty awesome and versatile tool, it isn’t perfect for every job, and so sometimes a macro is just what the doctor ordered. In the rest of this book, we’ll look at good reasons to use macros, despite the problems they can cause.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.93.175