How to define and use a macro in both CLJ and CLJS?

Hello,

After many (many many) trials and errors, I’ve finally managed to define a macro that I can use in a CLJC file, for both a CLJ/JVM project and a CLJS/Browser project. This post started as a question that I rewrote at least 3 times as things went on, until I found the solution. So I edited the post a little bit and I decided to post here the solution for my future self and anyone who struggles with the borrow checker compiler :wink:.

Problem statement

I have a file A.cljc that defines a lot of specs. It uses a custom spec macro where I defined an alternative to s/keys. The macro itself works fine. The macros makes use of a helper function. The goal is to be able to use the specs from A.cljc in both Clojurescript in the browser and Clojure in the JVM.

Failures…

I’ve tried so many things (all failed somehow) that I was lost. For example:

  • define the macro in A.cljc, and additionally and wrap the macro definition in #?(:clj (defmacro ...) reader conditional
  • define the macro in A.clj along with its helper function and (:require-macros [A]) from A.cljc. I’m sure I also copy-pasted the helper function in A.cljc, otherwise the helper function would be defined solely in a .clj file and unavailable in a CLJS runtime.
  • define the macro in M.clj and move its helper function in M.cljc, and (:require [M]) from A.cljc
  • try using https://github.com/cgrand/macrovich
  • so many combinations/variants of the above…

Good resources on the subject

I’ve read the post of @mfikes about the 2 files macro patterns, the official guide about ns forms, a post on the CLJS mailing list, other pages on the internet, the source code of @bhauman’s spell-spec, nothing seemed to work… until it worked!

The solution

Define the macro and its supporting function in a separate .cljc file (pattern took from the excellent spell-spec):

tracking_spec/macros.cljc

(ns tracking-spec.macros
  (:require [clojure.spec.alpha :as s]
            [spell-spec.alpha :refer [strict-keys]])
  #?(:cljs (:require-macros [tracking-spec.macros :refer [keys-gen-full]])))
;; notice the reader conditional, where CLJS compiler will require the macro specifically so that it's loaded correctly in a CLJS execution context

;; supporting function
(defn keys-gen-full*
  "Don't use it directly, use 'keys-gen-full"
  [keys-spec keys-spec-all-req]
  (reify
    s/Specize
    (specize* [s] s)
    (specize* [s _] s)

    s/Spec
    (conform* [_ x] (s/conform keys-spec x))
    (unform* [_ y] (s/unform keys-spec y))
    (explain* [_ path via in x] (s/explain* keys-spec path via in x))
    (gen* [_ overrides path rmap] (s/gen* keys-spec-all-req overrides path rmap))
    (with-gen* [_ gfn] (s/with-gen* keys-spec gfn))
    (describe* [_] (s/describe* keys-spec))))

;; macro wrapped in a :clj reader conditional to avoid some weird behavior of the CLJS compiler where it would define it as a function when compiling
#?(:clj
   (defmacro keys-gen-full
     "Like s/keys, but ensures that all optional keys are used when generating data."
     [& {:keys [req req-un opt opt-un]}]
     `(let [keys-spec#         (strict-keys :req ~req :req-un ~req-un :opt ~opt :opt-un ~opt-un)
            keys-spec-all-req# (strict-keys :req ~(vec (concat req opt)) :req-un ~(vec (concat req-un opt-un)))]
        (keys-gen-full* keys-spec# keys-spec-all-req#))))

Then simply use the macro with a normal :require.

tracking_spec/core.cljc

(ns tracking-spec.core
  (:require [clojure.spec.alpha :as s]
            [tracking-spec.macros :refer [keys-gen-full]]))

;; ... lots of specs, some looking like:
(s/def ::a-spec (keys-gen-full :req-un [::a] :opt-un [::b]))

Both the CLJ and CLJS project that use the spec library simply :require's it normally (I messed things here at some point where I used both :require and :require-macros from the CLJS project…)

(ns my-project.core
  (:require [tracking-spec.core :as tspec]))

This is solution that finally worked for me, after days of fighting the borrow checker spec and the CLJ/CLJS compilers . By the way, this problem was also a way to learn me some spec-fu. Big thanks @dnolen @mfikes and @bhauman for the guides/blog posts/libraries that guided me to the solution!

PS1: Note to the Clojureverse’s admins: I don’t have enough karma to add custom tags? Then could you add the tag macro (or macros, as you wish) to the post? Thanks in advance!

PS2: No offense meant to the people who enjoy Rust, it’s Friday :wink:

13 Likes