ClojureScript Macro Sugar

March 1, 2016

In ClojureScript, macros are handled a bit differently than in Clojure. In particular, the ns form supports :require-macros, along with some simplifying sugar and implicit loading behavior that this post will cover.




Primitives

For the sake of an example, let's assume that we have the following in src/foo/core.clj:

(ns foo.core)

(defmacro add
  [a b]
  `(+ ~a ~b))

To use this macro in some ClojureScript source, you can employ the :require-macros spec as

(ns bar.core
  (:require-macros foo.core))
  
(foo.core/add 2 3)

Now, :require-macros is designed to work a lot like :require. So, for example, you could directly refer the add symbol into your namespace:

(ns bar.core
  (:require-macros 
    [foo.core :refer [add]]))
  
(add 2 3)

An alternative to the above (which is infrequently seen, but worth covering for completeness), is :use-macros. In ClojureScript :require / :refer and :use / :only are essentially dual forms of each other. So, the ns form in the above could have just as well been written (ns bar.core (:use-macros [foo.core :only [add]])).

You could also set up a namespace alias foo:

(ns bar.core
  (:require-macros [foo.core :as foo]))
  
(foo/add 2 3)

Sugar

The examples above are all making use of the :require-macros primitive.

Frequently, though, you will be consuming code that comes from a library that offers both runtime code (functions and other defs) and macros, all from the same namespace.

So, continuing on our example, let's say that there is a src/foo/core.cljs file with

(ns foo.core)

(defn subtract
  [a b]
  (- a b))

Now if you wanted to use both add and subtract, you might do something like this:

(ns bar.core
  (:require-macros 
    [foo.core :refer [add]])
  (:require 
    [foo.core :refer [subtract]]))
  
(add 2 3)
(subtract 7 4)

But, there is a bit of ns-form sugar, :refer-macros that lets you write instead:

(ns bar.core
  (:require 
    [foo.core :refer [subtract] 
              :refer-macros [add]]))
  
(add 2 3)
(subtract 7 4)

The :refer-macros above really is just sugar, and it is algorithmically desugared into the previous form by the compiler.

Similarly, there is :include-macros sugar that you can use to signal that the macros namespace should be required using the same specifications as the runtime namespace. So for example, this works:

(ns bar.core
  (:require 
    [foo.core :as foo 
              :include-macros true]))

(foo/add 2 3)
(foo/subtract 7 4)

The above desugars into the more repetitively verbose primitive form:

(ns bar.core
  (:require-macros [foo.core :as foo])
  (:require [foo.core :as foo]))

(foo/add 2 3)
(foo/subtract 7 4)

Implicit Sugar

In cases where a runtime namespace being required internally requires its own macro namespace (meaning a namespace with the same name), then you implicitly get the :include-macros sugar for free.

To illustrate this, let's say our src/foo/core.cljs file instead looked like this:

(ns foo.core
  (:require-macros foo.core))

(defn subtract
  [a b]
  (- a b))

Now, you can consume things like this:

(ns bar.core
  (:require [foo.core :as foo]))

(foo/add 2 3)
(foo/subtract 7 4)

Implicit Refer

What about this nice-looking simplification?

(ns bar.core
  (:require 
    [foo.core :refer [add subtract]))

(add 2 3)
(subtract 7 4)

This won't work today, but it is the subject of a potential future enhancement. This would be nice sugar because it essentially smoothes over one remaining difference between ClojureScript and how macros can be consumed in Clojure.

Update July 3, 2016: Changes have landed for this feature!

Docs

If you are ever at a REPL and need a quick reference to the above topics, the docstring for the ns special form has been updated. The sugared forms are referred to as inline macro specification and the implicit sugar is referred to as implicit macro loading. A fairly comprehensive example of desugaring is included in the docstrring. In a pinch, (doc ns) is your friend.

Desugaring in the REPL

The compiler code that performs desugaring is not available via a stable public API. But you can still call it nonetheless. This is especially easy if you are in a bootstrapped REPL. Just do the following:

(require '[cljs.analyzer :as ana])

(defn desugar [[_ ns & specs]] 
  `(ns ~ns 
    ~@(ana/desugar-ns-specs specs)))

With this in place, you can evaluate things like

(desugar '(ns bar.core
  (:require [foo.core 
             :refer [subtract] 
             :refer-macros [add]])))

and get back

(ns bar.core (:require (foo.core :refer [subtract])) (:require-macros (foo.core :refer [add])))

I find being able to do this helps if there is an interesting corner case, and I really want to see what the compiler is doing with my ns form.

The require and require-macros REPL Specials

You can use require and require-macros to dynamically load code into your REPL. What's interesting is that the desugaring described in this post also works for these REPL specials.

This is an implementation detail, but it helps you see how this is accomplished: When you issue

(require-macros '[foo.core :as foo 
                  :refer [add]])

at the REPL, this is internally converted into an ns form that looks like

(ns cljs.user 
  (:require-macros 
    [foo.core :as foo :refer [add]]))

And, importantly, when you use require, a similar ns form is employed, and it is subject to all the desugaring described above.

Summary

Hopefully these detailed examples help clarify how desugaring works. The overall intent of sugaring is to simplify ClojureScript macro usage, but sometimes unpacking how these extra capabilities work leads to a better understanding for those times when you either want or need to know what is really going on.

Tags: ClojureScript