Two-File ClojureScript Namespace Pattern

August 12, 2018

This post describes a simple pattern for arranging ClojureScript namespaces, especially useful for those exposing macros delegating to runtime functionality.




Let’s say we want a macro that will do a reverse lookup in a map. For example, the following would yield 2:

(reverse-lookup (zipmap [1 2 3] [:a :b :c]) :b)

Furthermore, if the second argument evaluates to nil we will avoid evaluating the map argument (and thus not even call zipmap in the example above).

We put our macro in src/lib/core.clj:

(ns lib.core)

(defmacro reverse-lookup [m v]
  `(when-some [v# ~v]
     (get (inverse* ~m) v#)))

This macro makes use of an inverse* helper function, and as desired, expands to code which doesn’t evaluate the map argument m if v evaluates to nil.

If we were writing this macro for use in Clojure, we could define our inverse* helper function in the same file. But we need the inverse* function at runtime, so we'll put it in a ClojureScript namespace with the same name.

Here is src/lib/core.cljs:

(ns lib.core
  (:require-macros [lib.core]))

(defn inverse* [m]
  (into {} (map (fn [[k v]]
                  [v k])
             m)))

Notice that the ns form does a :require-macros on the lib.core namespace. This is key.

This two-file pattern makes our macro extremely easy to use from client namespaces:

cljs.user=> (require '[lib.core :as lib])
nil
cljs.user=> (lib/reverse-lookup (zipmap [1 2 3] [:a :b :c]) :b)
2

Notice that in the above, we simply required the lib.core namespace and used the macro. We didn’t need to use :require-macros or be concerned about whether reverse-lookup is a macro or a function, or that it uses a runtime function from the lib.core namespace.

This is great when we have macros that expand to employ helper runtime functions in their own namespace. But, what if we need to write macros that use functionality from some other namespace?

If you look at our inverse* helper function, it is essentially the same as clojure.set/map-invert. Let’s update our code to instead use this other namespace (clojure.set):

(defmacro reverse-lookup [m v]
  `(when-some [v# ~v]
     (get (clojure.set/map-invert ~m) v#)))

It is important that we qualify clojure.set/map-invert above. We also need to revise our runtime namespace to require clojure.set:

(ns lib.core
  (:require-macros [lib.core])
  (:require [clojure.set]))

With these changes, our reverse-lookup macro still works properly. This illustrates how we can arrange for our macro to arbitrarily make use of other namespaces: We need to qualify references in the macro definitions and be sure to require any used namespaces in our ClojureScript file.

This two-file pattern is fairly common. It’s also employed in many standard namespaces, including cljs.test, cljs.core.async, and cljs.spec.alpha.

If you’d like to learn more about the techniques that this pattern relies on, see the Namespaces guide.

Tags: ClojureScript