Chapter 8. Macros

ClojureScript, like Clojure, uses macros to extend the syntax of the language. Fundamentally, a macro is just a function that manipulates data structures. What makes macros special is that they are invoked during the compilation process, to manipulate the data structures representing ClojureScript source code. Many of ClojureScript’s core flow-control operators are implemented as macros, and you can write your own macros to extend the language.

Code as Data

Remember from Chapter 4 that all ClojureScript code is composed of data structures: lists, vectors, symbols, and so on. For example:

(println "Three plus four is" (+ 3 4))

We can read this expression as a list containing a symbol, a string, and another list. But to the ClojureScript compiler, that list represents a function call.

Macros allow you to manipulate the data structures in your code before they get to the compiler. This is very powerful: a macro can effectively rewrite code before it gets to the compiler.

Writing Macros

Macros are applied during the compilation process. They do not exist at runtime. Because the ClojureScript compiler is implemented in Clojure, ClojureScript macros must be written in Clojure, not ClojureScript. Fortunately, Clojure and ClojureScript are almost identical when it comes to manipulating data structures, so switching between the two languages is not difficult.

As an example, consider the when macro introduced in Chapter 4:

(when condition
  ;; ... expressions ...
  )

;; which expands to:
(if condition
  (do
    ;; ... expressions ...
    ))

The when macro is simply a way to avoid the extra do block when we want multiple expressions in an if expression. Usually this happens when the code inside the when macro is performing side effects.

To write a macro, first think about the expression you want to be able to write in your code. Second, think about what you want it to become. Finally, write a function that converts the first into the second. Here is a simple version of the when macro:

(defmacro when [condition & body]
  (list 'if condition
        (cons 'do body)))

Notice that a macro definition looks just like a function definition, but it starts with defmacro instead of defn. This function is variadic: it takes one argument called condition followed by any number of arguments that will be collected into the list called body. It then constructs a list starting with the symbol if, followed by the condition, followed by body with the do symbol inserted at the head.

Applying a macro is called macroexpansion, and it happens at the beginning of the ClojureScript compilation process. You can test it at the REPL with the macroexpand-1 function at the Clojure REPL. Remember, macros are written in Clojure, not ClojureScript, so you must write and test them at the Clojure REPL.

user=> (macroexpand-1 '(when (even? 2) (println "2 is even")))
(if (even? 2) (do (println "2 is even")))

Notice that we are calling the macroexpand-1 function on a quoted form. We don’t want to evaluate the when expression; we want to see what it will expand to during compilation. The macroexpand-1 function performs one round of macroexpansion. However, a macro can expand to code, which begins with another macro. To see the final result of all the expansions, you can call the macroexpand function, which keeps expanding macros until it reaches an expression that is not a macro.

There is also macroexpand-all, which recursively expands all the macros anywhere in an expression. It is available in the Clojure namespace clojure.walk. This macroexpand-all is not entirely correct because it doesn’t recognize special forms such as let, but it is usually adequate for debugging macro expressions.

Syntax-Quote

Macros manipulate data structures that represent code. However, as the code they produce grows more complex, it becomes tedious to manually construct the data structures to represent it. To help, Clojure has the syntax-quote operator to construct “templates” for expansion. Syntax-quote is written using the backtick (`) symbol. It behaves like the normal single quote in that it prevents evaluation, but syntax-quote also allows values to be unquoted.

Here is a version of when written with syntax-quote:

(defmacro [condition & body]
  `(if ~condition (do ~@body)))

Notice that we don’t have to do any manual construction, such as invoking list, as in the previous example. Instead, the syntax-quoted form looks similar to the form we ultimately want to produce. Within that form, we have unquoted the condition symbol by prefixing it with a tilde (~). We have used a variant of unquote called unquote-splicing on the body symbol. The unquote-splicing operator (~@) operates on lists by inserting the contents of the list at the expansion point, without the enclosing parentheses of the list itself. Unquote-splicing is like “unwrapping” a list before placing it in the expansion.

Auto-Gensyms

It is often necessary to create new symbols in the body of a macro, such as let bindings. To prevent these symbols from clashing with symbols already in use elsewhere around the code, Clojure’s macros provide auto-gensyms, or automatically-generated symbols, guaranteed to have unique names. These symbols are generated by placing a hash sign (#) after the symbol name. Auto-gensyms are only available within a syntax-quoted expression.

For example, here is a macro that expands to some debugging code:

(defmacro debug [expr]
  `(let [result# ~expr]
     (println "Evaluating:" '~expr)
     (println "Result:" result#)
     result#))

In this example, the debug macro takes a single expression and uses it twice. To avoid evaluating expr more than once, it has to create an intermediate let binding. The result# symbol will expand to an auto-gensym, which is guaranteed to have a unique name that doesn’t clash with any other symbols. Macroexpansion shows the result:

user=> (macroexpand-1 '(debug (println "hello")))
(clojure.core/let [result__6__auto__ (println "hello")]
  (clojure.core/println "Evaluating:" (quote (println "hello")))
  (clojure.core/println "Result:" result__6__auto__))

The debug macro also contains a clever trick: the “quote-unquote” in '~expr. This allows the expansion to print the literal code of expr without evaluating it.

Using Macros

Because macros are written in Clojure, they must be loaded differently in the ClojureScript compiler. To reference a macro from another namespace, add it to the ns declaration using the :require-macros form. For example:

(ns my-project.main
  (:require-macros [my-project.foo :as foo]))

(foo/my-macro)

This assumes that a Clojure source file is available on the classpath at my_project/foo.clj containing defmacro foo.

With the exception of the ns declaration, you generally do not need to think about whether you are calling a function or a macro in ClojureScript code. Many of the core flow-control structures of ClojureScript are implemented as macros (many of the core ClojureScript macros are actually the same as the core Clojure macros, invoked directly by the ClojureScript compiler!). The flow-control macros do not behave exactly like functions, because they can cause some of their arguments not to be evaluated. But well-written macros generally follow the behavior you expect: for example, ClojureScript’s and and or macros are “short-circuiting” just like the Boolean operators in JavaScript.

When to Write Macros

The first answer to “When should I write a macro?” is usually “Don’t!” Macros are the most powerful feature of a Lisp-like language, and the easiest to misuse. In general, you should always use functions and values as the primary units of abstraction in your code. Typically you only need macros in three cases:

  1. To do things functions cannot do. For example, the and conditional operation cannot be written as a function, because it needs to prevent evaluation of some of its arguments. Macros can control when and how their arguments are evaluated.[3]

  2. To add a layer of syntactic sugar. For example, the when macro doesn’t do anything different from what you can already accomplish with if and do, but it makes the syntax shorter and easier to read.

  3. To improve performance. Because macros are evaluated during compilation, they can potentially convert an expression into a more-efficient form before it reaches the compiler. The ClojureScript compiler uses macros internally to produce more efficient code, but you are unlikely to encounter this situation in everyday programming.

Summary

Macros are an extremely powerful language tool, so powerful that they are rarely needed in everyday programming. However, for advanced tasks, such as defining new control structures or embedding domain-specific languages, they can be invaluable. This chapter has barely scratched the surface of what macros can do. For more examples, refer to books about Clojure. For even deeper exploration of macros, look to books on Common Lisp, such as Paul Graham’s classic On Lisp, available free online. Note that most other Lisps use the comma character instead of tilde for unquote.



[3] Technically, you can prevent evaluation of function arguments by wrapping each argument in an anonymous function, but this is syntactically cumbersome.

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

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