Approaching Hygiene with the Gensym

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.

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

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