In order to avoid symbol capture issues like the one we just saw, Clojure gives us a few tools, all related to the gensym function. gensym’s job is simple: it produces a symbol with a unique name. The names will look funny because the name needs to be unique for the application, but that’s OK because we never need to type them into code:
advanced_mechanics/gensym_2.clj | |
| user=> (gensym) |
| ;=> G__671 |
| user=> (gensym) |
| ;=> G__674 |
| user=> (gensym "xyz") |
| ;=> xyz677 |
| user=> (gensym "xyz") |
| ;=> xyz680 |
As you can see, any given invocation of gensym gives a unique value back—so if you want to refer to the same one twice, you’ll need to hold onto the value with a let binding or something similar. These generated symbols (gensyms) are extremely useful for macros, but because they’re normal data, you can use them anywhere you’d use a symbol. In our make-adder macro earlier, we can’t have user/y as a function argument, and we just saw that we don’t want plain old y as a function argument, but we can use a gensym as a function argument:
advanced_mechanics/gensym_3.clj | |
| (defmacro make-adder [x] |
| (let [y (gensym)] |
| `(fn [~y] (+ ~x ~y)))) |
| |
| user=> y |
| 100 |
| user=> ((make-adder (+ y 3)) 5) |
| 108 |
Now this version uses the value of y that we’d expect as users of this macro. Notice that the let and gensym here are outside of the syntax-quote. It’s a bit unfortunate that this is so verbose—let’s use the more concise and built-in version. We’ll use a feature called the auto-gensym, which just looks like a normal symbol with a pound sign (#) at the end, like a reverse hashtag:
advanced_mechanics/gensym_4.clj | |
| (defmacro make-adder [x] |
| `(fn [y#] (+ ~x y#))) |
| |
| user=> y |
| 100 |
| user=> ((make-adder (+ y 3)) 5) |
| 108 |
This, and not any of the previous ways we’ve done it, should be the tool you reach for when you need to bind a name within a macro. There are several other very similar cases of binding symbols to values where we also need to use gensyms to construct macros safely:
advanced_mechanics/gensym_5.clj | |
| (defmacro safe-math-expression? [expression] |
| `(try ~expression |
| true |
| (catch ArithmeticException e# false))) |
| |
| ;; clojure.core/and |
| (defmacro and |
| ([] true) |
| ([x] x) |
| ([x & next] |
| `(let [and# ~x] |
| (if and# (and ~@next) and#)))) |
Bindings set up by special forms like let, letfn, and try’s catch clause have the same requirement as function arguments, so you should typically use the auto-gensym for these situations, too.
A lot of care has been taken in Clojure to make macro construction less error-prone. These variable-capture problems, along with the ability to get gensyms explicitly, have been around for a long time in Common Lisp, but it takes a bit of voodoo (see Doug Hoyte’s Let Over Lambda [Hoy08]) to get something like Clojure’s auto-gensym feature. It’s worth noting that if you wander into the dark forests of nesting syntax-quotes, you (a) may never return, and (b) may want to take a look at unify-gensyms from Zach Tellman’s Potemkin.[13]
Of course, anyone with a Scheme background is probably howling at this point because they have a hygienic macro system that makes unintended variable capture impossible. Allowing variable capture when we really, really want it makes Clojure’s macro system technically more dangerous than hygienic systems. By getting us most of the way there, Clojure gives us more safety than Common Lisp’s macro system, along with the power to do variable capture when it makes for an elegant solution.
3.139.239.41