ClojureScript Macros are a hurdle for most CLJS beginners and wrapping your head around how they work can be quite confusing. I’ll try to cover the basics you need to know to start writing your own macros.

First of all - ClojureScript macros are written in Clojure and run during the ClojureScript compilation on the JVM. A macro is just a function that takes code (as simple data) and generates other code (again simple data) out of it. The CLJS compiler will then turn the result into JS. This is done in Clojure so that the generated JS does not need a full ClojureScript compiler at runtime.

Self-hosted CLJS is capable of compiling CLJS at runtime and does support macros but requires at least 2MB of JS so it isn’t practical for most build targets. This post will not cover anything self-host related, as that is a topic for advanced users.

Step By Step

  1. Create a CLJ namespace, eg. my.util in src/main/my/util.clj (assuming src/main is one of the :source-paths). Note that this is a .clj file to create a Clojure namespace, not ClojureScript. Define the foo macro as you’d normally do in Clojure.

     (ns my.util)
    
     (defmacro foo [& body]
       ...)
    
  2. Create a CLJS namespace of the same name, so src/main/my/util.cljs and add a :require-macros for itself in the ns form.

     (ns my.util
       (:require-macros [my.util]))
    
  3. Use it.

     (ns my.app
       (:require [my.util :as util]))
    
     (util/foo 1 2 3)
     ;; :refer (foo) and (foo 1 2 3) would also work
    

Done.

Steps Explained

To explain why we do those steps we best go in reverse.

  • When the CLJS compiler processes the (util/foo 1 2 3) call it expands the util alias to its fully qualified (my.util/foo 1 2 3) form

  • The compiler looks up my.util/foo. Typically the compiler would only look for that var in the ClojureScript environment (so what was defined in the .cljs files). The my.util namespace however had the :require-macros for itself which tells the compiler to also look for macros of the same name

  • The my.util/foo CLJ macro is found and the compiler will expand the form using that macro

  • The CLJS compiler continues with the expanded form

The Old Way

Before CLJS-948 macros required a bit more ceremony. Thus you still see these patterns in older code. Nowadays all macros should be written as described above.

In the above example we are using the macro self-require directive (ie. :require-macros) to inform the CLJS compiler about macros that supplement a given CLJS namespace. This requires that there actually is a matching CLJS namespace, but in the past the self-require trick didn’t exist so macros sort of existed on their own.

It was common to create dedicated .macros namespaces (eg. as seen in cljs.core.async.macros). core.async provides a go macro should you can nowadays access in two ways.

The old way

(ns my.app
  (:require-macros [cljs.core.async.macros :refer (go)])
  (:require [cljs.core.async :as async :refer (chan)]))

(go :foo)

or the modern way

(ns my.app
  (:require [cljs.core.async :as async :refer (chan go)]))

(go :foo)

The problem with the old way was that the consumer of a library had to have special knowledge about macros and how to consume them. In the example above the user had to know that chan was a regular var and that go was a macro. In the modern way the compiler can figure this out on its own and all it took was a matching namespace with the :require-macros self-require trick. If a regular CLJS “var” has a defmacro of the same name in CLJ it will expand the macro first if applicable.

Macro Limitations

Macros can pretty much do everything that regular CLJ macros can do but since you are basically dealing with 2 separate versions of the same namespace there are some things to watch out for.

Gotcha #1: Namespace Aliases and Dependencies

Since the macros run in CLJ and not CLJS the namespace aliases you configured in CLJS will not work in the macro. It is recommended to use fully qualified names if you need to access code from other namespaces. Defining the :require in CLJS ensures that the clojure.string code will actually be available at runtime. If only the CLJ variant had that :require the CLJS compiler might not provide that namespace.

;; CLJS
(ns my.app
  (:require [my.util :refer (foo)]))

(foo :hello "world")

;; CLJS 
(ns my.util
  (:require-macros [my.util])
  (:require [clojure.string :as str]))

;; CLJ
(ns my.util)

;; this would fail since the CLJ namespace doesn't know about the str alias
(defmacro foo [key value]
  `{:key ~key
    :value (str/upper-case ~value)})

;; so instead use the fully qualified name
(defmacro foo [key value]
  `{:key ~key
    :value (clojure.string/upper-case ~value)})

There may be cases where there are actually matching CLJ namespace that you could require in the CLJ variant but if you want to be safe just use the fully qualified name.

Gotcha #2: Caching and :parallel-build

The ClojureScript compiler might be caching the result of the compilation so you should try to avoid side-effects in macros at all cost. The compiler might also be using multiple threads for compilation so if you use side-effects that rely on specific ordering things might get messy.

If you cannot avoid side-effects make sure to turn off caching and probably :parallel-build.

Gotcha #3: Macro-Support code

The CLJ “macro” namespace is just a regular Clojure namespace. You can use all the code you want in it during macro expansion but if your macro expands to code that needs to be called at runtime that code must be defined in the CLJS variant instead.

;; CLJS
(ns my.util
  (:require-macros [my.util])
  (:require [clojure.string :as str]))

(defn my-helper [m] ...)

;; CLJ
(ns my.util)

(defmacro foo [key value]
  `(my-helper {:key ~key
               :value (clojure.string/upper-case ~value)}))

This will expand to (my.util/my-helper ...) which the CLJS compiler will find properly. CLJS code cannot access a defn defined in the CLJ namespace so make sure everything is where it needs to be. You can use a fully qualified symbol for my-helper but the syntax quote (ie. the ` backtick) automatically applies the current namespace (ie. my.util) to all unqualified symbols so it can be omitted here.

Gotcha #4: CLJ Macros

So far this post assumed that the macro only had to generate CLJS code and didn’t need to work in regular CLJ code. Some libraries however will want to work on both platforms so they need a way to detect whether they are supposed to expand to CLJS code or CLJ code. This can be done by inspecting the special &env variable during macro expansion. Note that you cannot do this with reader conditionals when using .cljc files.

(ns my.util)

(defmacro foo [key value]
  (if (:ns &env) ;; :ns only exists in CLJS
   `(cljs-variant ...)
   `(clj-variant ...))

Gotcha #5: CLJC

CLJC can be difficult to get right since you are defining two completely independent namespaces (CLJ+CLJS) at the same time in one file.

I strongly recommend writing your macros in 2 separate files until you feel comfortable with that. I still do it for 100% of my macros.

A lot of the macros in the wild out there written in CLJC do it wrong in some way. Most of the time this isn’t a big problem since the Closure Compiler gets rid of the code that “leaked” in the CLJS side of things. Nevertheless if you must use CLJC files you should make sure that all CLJ related code is properly behind #?(:clj ...) conditionals.

(ns my.util
  #?(:cljs (:require-macros [my.util])))

(defn my-helper [m] ...)

(defn macro-helper [k v] ...)

(defmacro foo [key value]
  `(my-helper ~(macro-helper key value))

It is somewhat easy to overlook that in this example macro-helper is actually called during macro expansion and not at runtime. The macro-helper defn however was not behind a reader conditional so it will be compiled as part of the CLJS variant and actually exist as a regular function at runtime. That doesn’t necessarily hurt anyone but depending on how much code is “leaked” that way may make compilation slower or may lead to actual additional bytes in a release build if the Closure Compiler was not able to eliminate all of it (eg. if you used defmethod for CLJ multi-method).

Summary

  • ClojureScript macros are written in Clojure and run on the JVM
  • Define macros by creating a CLJ and CLJS namespace of the same name, where the CLJS variant has a :require-macros directive in its ns
  • CLJC is harder to get right, avoid it if you can
  • Writing and using macros isn’t all that difficult ;)