Making Macros Simpler

The unless macro is a great simple example, but most macros are more complex. In this section, we’ll build a set of increasingly complex macros, introducing Clojure features as we go. For your reference, the following table summarizes the features introduced.

Form

Description

foo#

Auto-gensym: Inside a syntax-quoted section, create a unique name prefixed with foo.

(gensym prefix?)

Create a unique name, with optional prefix.

(macroexpand form)

Expand form with macroexpand-1 repeatedly until the returned form is no longer a macro.

(macroexpand-1 form)

Show how Clojure will expand form.

(list-frag? ~@form list-frag?)

Splicing unquote: Use inside a syntax quote to splice an unquoted list into a template.

‘form

Syntax quote: Quote form, but allow internal unquoting so that form acts as a template. Symbols inside form are resolved to help prevent inadvertent symbol capture.

~form

Unquote: Use inside a syntax quote to substitute an unquoted value.

First let’s build a replica of Clojure’s .. macro. We’ll call it chain, since it chains a series of method calls. Here are some sample expansions of chain:

Macro Call

Expansion

(chain arm getHand)

(. arm getHand)

(chain arm getHand getFinger)

(. (. arm getHand) getFinger)

Begin by implementing the simple case where the chain calls only one method. The macro needs only to make a simple list:

 ; chain reimplements Clojure's .. macro
 (​defmacro​ chain [x form]
  (list ​'.​ x form))

chain needs to support any number of arguments, so the rest of the implementation should define a recursion. The list manipulation becomes more complex, since you need to build two lists and concat them together:

 (​defmacro​ chain
  ([x form] (list ​'.​ x form))
  ([x form & more] (concat (list ​'chain​ (list ​'.​ x form)) more)))

Test chain using macroexpand to make sure it generates the correct expansions:

 (macroexpand '(chain arm getHand))
 -> (. arm getHand)
 
 (macroexpand '(chain arm getHand getFinger))
 -> (. (. arm getHand) getFinger)

The chain macro works fine as written, but it’s difficult to read the expression that handles more than one argument:

 (concat (list ​'chain​ (list ​'.​ x form)) more)))

The definition of chain oscillates between macro code and the body to be generated. The intermingling of the two makes the entire thing hard to read. And this is just a baby of a form, only one line in length. As macro forms grow more complex, assembly functions such as list and concat quickly obscure the meaning of the macro.

One solution to this kind of problem is a templating language. If macros were created from templates, you could take a “fill-in-the-blanks” approach to creating them. The definition of chain might look like this:

 ; hypothetical templating language
 (​defmacro​ chain
  ([x form] (. ${x} ${form}))
  ([x form & more] (chain (. ${x} ${form}) ${more})))

In this hypothetical templating language, the ${} lets you substitute arguments into the macro expansion.

Notice how much easier the definition is to read and how it clearly shows what the expansion will look like.

Syntax Quote, Unquote, and Splicing Unquote

Clojure macros support templating without introducing a separate language. The syntax quote character, which is a backquote (), works almost like normal quoting. But inside a syntax-quoted list, the unquote character (~, a tilde) turns quoting off again. The overall effect is templates that look like this:

 (​defmacro​ chain [x form]
  `(. ~x ~form))

Test that this new version of chain can correctly generate a single method call:

 (macroexpand '(chain arm getHand))
 -> (. arm getHand)

Unfortunately, the syntax quote/unquote approach won’t quite work for the multiple-argument variant of chain:

 ; Does not quite work
 (​defmacro​ chain
  ([x form] `(. ~x ~form))
  ([x form & more] `(chain (. ~x ~form) ~more)))

When you expand this chain, the parentheses aren’t quite right:

 (macroexpand '(chain arm getHand getFinger))
 -> (. (. arm getHand) (getFinger))

The last argument to chain is a list of more arguments. When you drop more into the macro “template,” it has parentheses because it’s a list. But you don’t want these parentheses; you want more to be spliced into the list. This comes up often enough that there is a reader macro for it: splicing unquote (~@). Rewrite chain using splicing unquote to splice in more:

 (​defmacro​ chain
  ([x form] `(. ~x ~form))
  ([x form & more] `(chain (. ~x ~form) ~@more)))

Now, the expansion should be spot on:

 (macroexpand '(chain arm getHand getFinger))
 -> (. (. arm getHand) getFinger)

Many macros follow the pattern of chain, aka Clojure ..

  1. Begin the macro body with a syntax quote () to treat the entire thing as a template.
  2. Insert individual arguments with an unquote (~).
  3. Splice in more arguments with splicing unquote (~@).

The macros we’ve built so far have been simple enough to avoid creating any bindings with let or binding. Let’s create such a macro next.

Creating Names in a Macro

Clojure has a time macro that times an expression, writing the elapsed time to the console:

 (time (str ​"a"​ ​"b"​))
 | ​"Elapsed time: 0.06 msecs"
 -> ​"ab"

Let’s build a variant of time called bench, designed to collect data across many runs. Instead of writing to the console, bench will return a map that includes both the return value of the original expression and the elapsed time.

The best way to begin writing a macro is to write its desired expansion by hand. bench should expand like this:

 ; (bench (str ​"a"​ ​"b"​))
 ; should expand to
 (​let​ [start (System/nanoTime)
  result (str ​"a"​ ​"b"​)]
  {:result result :elapsed (- (System/nanoTime) start)})
 
 -> {:elapsed 61000, :result ​"ab"​}

The let binds start to the start time and then executes the expression to be benched, binding it to result. Finally, the form returns a map including the result and the elapsed time since start.

With the expansion in hand, you can now work backward and write the macro to generate the expansion. Using the technique from the previous section, try writing bench using syntax quoting and unquoting:

 ; This won't work
 (​defmacro​ bench [expr]
  `(​let​ [start (System/nanoTime)
  result ~expr]
  {:result result :elapsed (- (System/nanoTime) start)}))

If you try to call this version of bench, Clojure will complain:

 (bench (str ​"a"​ ​"b"​))
 -> java.lang.Exception​:​ Can​'t​ ​let​ qualified name​:​ examples.macros/start

Clojure is accusing you of trying to let a qualified name, which is illegal. Calling macroexpand-1 confirms the problem:

 (macroexpand-1 '(bench (str ​"a"​ ​"b"​)))
 -> (clojure.core/let [examples.macros/start (System/nanoTime)
  examples.macros/result (str ​"a"​ ​"b"​)]
  {:elapsed (clojure.core/- (System/nanoTime) examples.macros/start)
  :result examples.macros/result})

When a syntax-quoted form encounters a symbol, it resolves the symbol to a fully qualified name. At the moment, this seems like an irritant, because you want to create local names, specifically start and result. But Clojure’s approach protects you from a nasty macro bug called symbol capture.

What would happen if macro expansion did allow the unqualified symbols start and result, and then bench was later used in a scope where those names were already bound to something else? The macro would capture the names and bind them to different values, with bizarre results. If bench captured its symbols, it would appear to work fine most of the time. Adding 1 and 2 gives you 3:

 (​let​ [a 1 b 2]
  (bench (+ a b)))
 
 -> {:result 3, :elapsed 39000}

…until the unlucky day that you picked a local name like start, which collided with a name inside bench:

 (​let​ [start 1 end 2]
  (bench (+ start end)))
 
 -> {:result 1228277342451783002, :elapsed 39000}

bench captures the symbol start and binds it to (System/nanoTime). All of a sudden, “1 plus 2” seems to equal 1228277342451783002.

Clojure’s insistence on resolving names in macros helps protect you from symbol capture, but you still don’t have a working bench. You need some way to introduce local names, ideally unique ones that can’t collide with any names used by the caller.

Clojure provides a reader form for creating unique local names. Inside a syntax-quoted form, you can append an octothorpe (#) to an unqualified name, and Clojure will create an autogenerated symbol, or auto-gensym: a symbol based on the name plus an underscore and a unique ID. Try it at the REPL:

 `foo#
 foo__1004

With automatically generated symbols at your disposal, it’s easy to implement bench correctly:

 (​defmacro​ bench [expr]
  `(​let​ [start# (System/nanoTime)
  result# ~expr]
  {:result result# :elapsed (- (System/nanoTime) start#)}))

Test it at the REPL:

 (bench (str ​"a"​ ​"b"​))
 -> {:elapsed 63000, :result ​"ab"​}

Clojure makes it easy to generate unique names, but if you’re determined, you can still force symbol capture. The sample code for the book includes an evil-bench that shows a combination of syntax quoting, quoting, and unquoting that leads to symbol capture. Don’t use symbol capture unless you have a thorough understanding of macros.

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

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