Taxonomy of Macros

Now that you’ve written several macros, we can restate the rules of Macro Club with more supporting detail.

The first rule of Macro Club is, Don’t Write Macros. Macros are complex. If you can avoid that complexity, you should.

The second rule of Macro Club is, Write Macros If That Is the Only Way to Encapsulate a Pattern. As you’ve seen, the patterns that resist encapsulation tend to arise around special forms, which are irregularities in a language. So the second rule can also be called the Special Form Rule.

Special forms have special powers that you, the programmer, do not have:

  • Special forms provide the most basic flow control structures, such as if and recur. All flow control macros must eventually call a special form.

  • Special forms provide direct access to Java. When you call Java from Clojure, you’re going through at least one special form, such as the . (dot) or new.

  • Names are created and bound through special forms, whether defining a var with def, creating a lexical binding with let, or creating a dynamic binding with binding.

As powerful as they are, special forms are not functions. They can’t do some things that functions can do. You cannot apply a special form, store a special form in a var, or use a special form as a filter with the sequence library. In short, special forms are not first-class citizens of the language.

The specialness of special forms could be a major problem and lead to repetitive, unmaintainable patterns in your code. But macros neatly solve the problem, because you can use macros to generate special forms. In a practical sense, all language features are first-class features at macro expansion time.

Macros that generate special forms are often the most difficult to write but also the most rewarding. As if by magic, such macros seem to add new features to the language.

The exception to the Macro Club rules is caller convenience: you can write any macro that makes life easier for your callers when compared with an equivalent function. Because macros don’t evaluate their arguments, callers can pass raw code to a macro, instead of wrapping the code in an anonymous function. Or, callers can pass unescaped names, instead of quoted symbols or strings.

We have reviewed the macros in Clojure and contrib libraries, and almost all of them follow the rules of Macro Club. Also, they fit into one or more of the categories in the following table, which shows the taxonomy of Clojure macros.

Justification

Category

Examples

Special form

Conditional evaluation

when, when-not, and, or, comment

Special form

Defining vars

defn, defmacro, defmulti, defstruct, declare

Special form

Java interop

.., doto, import-static

Caller convenience

Postponing evaluation

lazy-cat, lazy-seq, delay

Caller convenience

Wrapping evaluation

with-open, dosync, with-out-str, time, assert

Caller convenience

Avoiding a lambda

(Same as for “Wrapping evaluation”)

Let’s examine each of the categories in turn.

Conditional Evaluation

Because macros do not immediately evaluate their arguments, they can be used to create custom control structures. You’ve already seen this with the unless example in Writing a Control Flow Macro.

Macros that do conditional evaluation tend to be fairly simple to read and write. They follow a common form: evaluate some argument (the condition); then, based on that evaluation, pick which other arguments to evaluate, if any. A good example is Clojure’s and:

1: (​defmacro​ and
2:  ([] true)
3:  ([x] x)
4:  ([x & rest]
5:  `(​let​ [and# ~x]
6:  (​if​ and# (and ~@rest) and#))))

and is defined recursively. The zero- and one-argument bodies set up base cases:

  • For no arguments, return true.
  • For one argument, return that argument.

For two or more arguments, and uses the first argument as its condition, evaluating it on line 5. Then, if the condition is true, and proceeds to evaluate the remaining arguments by recursively anding the rest (line 6).

To short-circuit evaluation after the first non-true value is encountered, and must be a macro. Unsurprisingly, and has a close cousin macro, or. Their signatures are the same:

 (and & exprs)
 (or & exprs)

The difference is that and stops on the first logical false, while or stops on the first logical true:

 (and 1 0 nil false)
 -> nil
 
 (or 1 0 nil false)
 -> 1

The all-time, short-circuit evaluation champion is the comment macro:

 (comment & exprs)

comment never evaluates any of its arguments and is sometimes used at the end of a source code file to demonstrate the usage of an API.

For example, the Clojure inspector library ends with the following comment, demonstrating the use of the inspector:

 (comment
 
 (load-file ​"src/inspector.clj"​)
 (refer ​'inspector​)
 (inspect-tree {:a 1 :b 2 :c [1 2 3 {:d 4 :e 5 :f [6 7 8]}]})
 (inspect-table [[1 2 3][4 5 6][7 8 9][10 11 12]])
 
 )

Notice the lack of indentation. This would be nonstandard in most Clojure code but is useful in comment, whose purpose is to draw attention to its body.

Creating Vars

Clojure vars are created by the def special form. Anything else that creates a var must eventually call def. So, for example, defn, defmacro, and defmulti are all themselves macros.

To demonstrate writing macros that create vars, we’ll look at two macros that are also part of Clojure: defstruct and declare.

Clojure provides a low-level function for creating structs called create-struct. Note that structs are effectively deprecated now in favor of records, but defstruct is still an instructive macro example.

 (create-struct & key-symbols)

Use create-struct to create a person struct:

 (​def​ person (create-struct :first-name :last-name))
 -> #​'user/person

create-struct works, but it’s visually noisy. Given that you often want to immediately def a new struct, you’ll typically call defstruct, which combines def and create-struct in a single operation:

 (​defstruct​ name & key-symbols)

defstruct is a simple macro, and it’s already part of Clojure:

 (​defmacro​ ​defstruct
  [name & keys]
  `(​def​ ~name (create-struct ~@keys)))

This macro takes advantage of several macro features: delayed evaluation of the symbol name, splicing of keys, and rewriting the expressions at compile time rather than a runtime invocation of def.

defstruct makes a single line easier to read, but some macros can also condense many lines into a single form. Consider the issue of forward declarations. You’re writing a program that needs forward references to vars a, b, c, and d. You can call def with no arguments to define the var names without an initial binding:

 (​def​ a)
 (​def​ b)
 (​def​ c)
 (​def​ d)

But this is tedious and wastes a lot of vertical space. The declare macro takes a variable list of names and defs each name for you:

 (declare & names)

Now you can declare all the names in a single compact form:

 (declare a b c d)
 -> #​'user/d

The implementation of declare is built into Clojure:

 (​defmacro​ declare
  [& names] `(do ~@(map #(list ​'def​ %) names)))

Let’s analyze declare from the inside out. The anonymous function #(list ’def %) is responsible for generating a single def. Test this form alone at the REPL:

 (#(list ​'def​ %) ​'a​)
 -> (​def​ a)

The map invokes the inner function once for each symbol passed in. Again, you can test this form at the REPL:

 (map #(list ​'def​ %) '[a b c d])
 -> ((​def​ a) (​def​ b) (​def​ c) (​def​ d))

The leading do makes the entire expansion into a single legal Clojure form:

 `(do ~@(map #(list ​'def​ %) '[a b c d]))
 -> (do (​def​ a) (​def​ b) (​def​ c) (​def​ d))

Substituting ’[a b c d] in the previous form is the manual equivalent of testing the entire macro with macroexpand-1:

 (macroexpand-1 '(declare a b c d))
 -> (do (​def​ a) (​def​ b) (​def​ c) (​def​ d))

Many of the most interesting parts of Clojure are macros that expand into special forms involving def. We’ve explored a few here, but you can read the source of any of them. Most of them live at src/clj/clojure/core.clj in the Clojure source distribution.

Java Interop

Clojure programs call into Java via the . (dot), new, and set! special forms. However, idiomatic Clojure code often uses macros such as .. (threaded member access) and doto to simplify forms that call Java.

You (or anyone else) can extend how Clojure calls Java by writing a macro. Consider the following scenario. You’re writing code that uses several of the constants in java.lang.Math:

 Math/PI
 -> 3.141592653589793
 (Math/pow 10 3)
 -> 1000.0

In a longer segment of code, the Math/ prefix would quickly become distracting, so it would be nice if you could say simply PI and pow. Clojure doesn’t provide a direct way to do this, but you could define a bunch of vars by hand:

 (​def​ PI Math/PI)
 -> #​'user/PI
 (​defn​ pow [b e] (Math/pow b e))
 -> #​'user/pow

Stuart Sierra[41] automated the boilerplate with the import-static macro:

 (examples.import-static/import-static class & members)

import-static imports static members of a Java class as names in the local namespace. Use import-static to import the members you want from Math.

 (require '[examples.import-static :refer [import-static]])
 (import-static java.lang.Math PI pow)
 -> nil
 
 PI
 -> 3.141592653589793
 
 (pow 10 3)
 -> 1000.0

Postponing Evaluation

Most sequences in Clojure are lazy. When you’re building a lazy sequence, you often want to combine several forms whose evaluation is postponed until the sequence is forced. Since evaluation is not immediate, a macro is required.

You’ve already seen such a macro in Lazy and Infinite Sequences: lazy-seq. Another example is delay:

 (delay & exprs)

When you create a delay, it holds on to its exprs and does nothing with them until it’s forced to. Try creating a delay that simulates a long calculation by sleeping:

 (​def​ slow-calc (delay (Thread/sleep 5000) ​"done!"​))
 -> #​'user/slow-calc

To actually execute the delay, you must force it:

 (force x)

Try forcing your slow-calc a few times:

 (force slow-calc)
 -> ​"done!"
 (force slow-calc)
 -> ​"done!"

The first time you force a delay, it executes its expressions and caches the result. Subsequent forces simply return the cached value.

The macros that implement lazy and delayed evaluation all call Java code in clojure.jar. In your own code, you should not call such Java APIs directly. Treat the lazy/delayed evaluation macros as the public API, and treat the Java classes as implementation detail that’s subject to change.

Wrapping Evaluation

Many macros wrap the evaluation of a set of forms, adding some special semantics before and/or after the forms are evaluated. You’ve already seen several examples of this kind of macro:

  • time starts a timer, evaluates forms, and then reports how long they took to execute.

  • let and binding establish bindings, evaluate some forms, and then tear down the bindings.

  • with-open takes an open file (or other resource), executes some forms, and then makes sure the resource is closed in a finally block.

  • dosync executes forms within a transaction.

Another example of a wrapper macro is with-out-str:

 (with-out-str & exprs)

with-out-str temporarily binds *out* to a new StringWriter, evaluates its exprs, and then returns the string written to *out*. with-out-str makes it easy to use print and println to build strings on the fly:

 (with-out-str (print ​"hello, "​) (print ​"world"​))
 -> ​"hello, world"

The implementation of with-out-str has a simple structure that can act as a template for writing similar macros:

1: (​defmacro​ with-out-str
2:  [& body]
3:  `(​let​ [s# (new java.io.StringWriter)]
4:  (binding [*out* s#]
5:  ~@body
6:  (str s#))))

Wrapper macros usually take a variable number of arguments (line 2), which are the forms to be evaluated. They then proceed in three steps:

  1. Setup: Create some special context for evaluation, introducing bindings with let (line 3) and bindings (line 4) as necessary.

  2. Evaluation: Evaluate the forms (line 5). Since there is typically a variable number of forms, insert them via a splicing unquote: ~@.

  3. Teardown: Reset the execution context to normal and return a value as appropriate (line 6).

When writing a wrapper macro, always ask yourself whether you need a finally block to implement the teardown step correctly. For with-out-str, the answer is No, because both let and binding take care of their own cleanup. If, however, you’re setting some global or thread-local state via a Java API, you’ll need a finally block to reset this state.

This talk of mutable state leads to another observation. Any code whose behavior changes when executed inside a wrapper macro is obviously not a pure function. print and println behave differently based on the value of *out* and so are not pure functions. Macros that set a binding, such as with-out-str, do so to alter the behavior of an impure function somewhere.

Not all wrappers change the behavior of the functions they wrap. You’ve already seen time, which times a function’s execution. Another example is assert:

 (assert expr)

assert tests an expression and raises an exception if it’s not logically true:

 (assert (= 1 1))
 -> nil
 
 (assert (= 1 2))
 -> java.lang.Exception​:​ Assert failed​:​ (= 1 2)

Macros like assert and time violate the first rule of Macro Club to avoid unnecessary lambdas.

Avoiding Lambdas

For historical reasons, anonymous functions are often called lambdas. Sometimes a macro can be replaced by a function call, with the arguments wrapped in a lambda. For example, the bench macro from Syntax Quote, Unquote, and Splicing Unquote does not need to be a macro. You can write it as a function:

 (​defn​ bench-fn [f]
  (​let​ [start (System/nanoTime)
  result (f)]
  {:result result :elapsed (- (System/nanoTime) start)}))

However, if you want to call bench-fn, you must pass it a function that wraps the form you want to execute. The following code shows the difference:

 ; macro
 (bench (+ 1 2))
 -> {:elapsed 44000, :result 3}
 
 ; function
 (bench-fn (​fn​ [] (+ 1 2)))
 -> {:elapsed 53000, :result 3}

For things like bench, macros and anonymous functions are near substitutes. Both prevent immediate execution of a form. However, the anonymous function approach requires more work on the part of the caller, so it’s OK to break the first rule and write a macro instead of a function.

Another reason to prefer a macro for bench is that bench-fn is not a perfect substitute; it adds the overhead of an anonymous function call at runtime. Since bench’s purpose is to time things, you should avoid this overhead.

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

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