Secret Macro Voodoo

In addition to all of the syntax-quoting tools that Clojure provides for both macros and normal programming, we have two special values, &form and &env, that are available only within macros. Both of these allow us to introspect a bit on the way a macro gets used. Let’s see what information we have available when we use them:

advanced_mechanics/secret_macro_variables_1.clj
 
(​defmacro​ info-about-caller []
 
(pprint {:form &form :env &env})
 
`(​println​ ​"macro was called!"​))
 
 
user=> (info-about-caller)
 
;{:form (info-about-caller), :env nil}
 
;macro was called!
 
;=> nil
 
user=> (​let​ [foo ​"bar"​] (info-about-caller))
 
;{:form (info-about-caller),
 
; :env {foo #<LocalBinding clojure.lang.Compiler$LocalBinding@23ef55fb>}}
 
;macro was called!
 
;=> nil
 
user=> (​let​ [foo ​"bar"​ baz ​"quux"​] (info-about-caller))
 
;{:form (info-about-caller),
 
; :env
 
; {baz #<LocalBinding clojure.lang.Compiler$LocalBinding@3f68eac0>,
 
; foo #<LocalBinding clojure.lang.Compiler$LocalBinding@55ab9655>}}
 
;macro was called!
 
;=> nil

The &env value seems pretty magical: it’s a map of local variables, where the keys are symbols and the values are instances of some class in the Clojure compiler internals. It turns out that if we really wanted to get crazy, this gives us access to all sorts of interesting data during macroexpansion—things like the Java types of the arguments and how the locals were initialized. People typically use &env to just look at the keys (which are symbols), and inject them into the expanded macro. If we want to get ahold of a map of local names to local values, we could do something like this:

advanced_mechanics/secret_macro_variables_2.clj
 
(​defmacro​ inspect-caller-locals []
 
(​->>​ (​keys​ &env)
 
(​map​ (​fn​ [k] [`'~k k]))
 
(​into​ {})))
 
 
user=> (inspect-caller-locals)
 
{}
 
user=> (​let​ [foo ​"bar"​ baz ​"quux"​] (inspect-caller-locals))
 
{baz ​"quux"​, foo ​"bar"​}

There’s a little bit of hairy quoting to get through here: ‘’~k. If we think through this carefully, though, it makes complete sense. We want to produce a quoted symbol for each local variable in the macroexpansion, but we want those quoted symbols to be the same as the ones in the &env map. This is getting a bit too fancy, but you’ll see this kind of quoting in the wild sometimes, so it’s worth wrapping your head around. However, a couple of equivalent, possibly more readable ways to say ‘’~k are ‘(quote ~k) and (list ’quote k):

advanced_mechanics/secret_macro_variables_3.clj
 
(​defmacro​ inspect-caller-locals-1 []
 
(​->>​ (​keys​ &env)
 
(​map​ (​fn​ [k] [`(​quote​ ~k) k]))
 
(​into​ {})))
 
 
(​defmacro​ inspect-caller-locals-2 []
 
(​->>​ (​keys​ &env)
 
(​map​ (​fn​ [k] [(​list​ '​quote​ k) k]))
 
(​into​ {})))
 
 
user=> (inspect-caller-locals-1)
 
{}
 
user=> (inspect-caller-locals-2)
 
{}
 
user=> (​let​ [foo ​"bar"​ baz ​"quux"​] (inspect-caller-locals-1))
 
{baz ​"quux"​, foo ​"bar"​}
 
user=> (​let​ [foo ​"bar"​ baz ​"quux"​] (inspect-caller-locals-2))
 
{baz ​"quux"​, foo ​"bar"​}

These all behave exactly the same, so it’s a matter of preference which one you’d choose. Keep in mind that whenever you encounter a confusing-looking quoting composition in a macro, you can always isolate the quoting bits at the REPL to see how they behave.

The &form special variable is a bit more straightforward. It contains the expression that was used to call the macro, which will always be a list since lists are how you call things in Clojure. This doesn’t really appear to buy you much, because as the macro author you already know what the name of the macro is, and you already have access to the argument expressions that are passed in:

advanced_mechanics/secret_macro_variables_4.clj
 
(​defmacro​ inspect-called-form [& arguments]
 
{:form (​list​ '​quote​ (​cons​ 'inspect-called-form arguments))})
 
 
user=> (inspect-called-form 1 2 3)
 
;=> {:form (inspect-called-form 1 2 3)}

There are several additional benefits of having &form available, though. First, there’s duplication in the way we did things previously, so if you change your mind about the macro name, you have to change two names in the macro definition. This definition is a lot of code as well—it’s just more convenient to use &form than to type that whole expression whenever we want to inspect the details of the macro call. One of the really big benefits that makes &form special is the fact that when we have the actual form available to us, we also have all of the metadata attached to it:

advanced_mechanics/secret_macro_variables_5.clj
 
(​defmacro​ inspect-called-form [& arguments]
 
{:form (​list​ '​quote​ &form)})
 
 
user=> ^{:doc ​"this is good stuff"​} (inspect-called-form 1 2 3)
 
;=> {:form (inspect-called-form 1 2 3)}
 
user=> (​meta​ (:form *1))
 
;=> {:doc "this is good stuff", :line 1, :column 1}

This can be kind of a big deal! It means that we can improve error messaging in macros by including line and column number information—this is by far the most common use of &form. But I’m sure you can imagine some other fancy ways to use metadata on expressions to give information to the macros that consume them.

Whew!

In this chapter, we’ve looked at all of the most advanced bits of macro construction. There’s much more to know, but not in terms of syntax! At this point you already have the building blocks that will allow you to construct any macro you want to. In the rest of this book, you’ll see how and why to write macros in your day-to-day work. But first, you’ll see in the next chapter why macros aren’t the solution for every problem, and how things can go wrong if you treat them as though they are.

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

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