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

macros
clojurescript
clojure_spec

#1

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:


#2

Thank you @martinklepsch for the new tag macros.


#3

Can confirm your final solution is what I use in my projects as well (except for the :refers).l went through the same pain but in the end it works well. Thanks a lot for sharing, I think it is a big gift to the community - I think I will never forget how hard it was to come up with this .cljc pattern :slight_smile:


#4

Happy you find it useful :+1: that was the whole point.


#5

Hi. Thanks for sharing.

One extra twist to this would be if you want to define and use a macro in both clj and cljs AND you need to include reader conditionals in that macro. Macrovich (pretty much a one-liner library so source can easily be read) helped me do this - again after a similar amount of tearing my hair out as you had ;-).


#6

Indeed, I also tried using Macrovich too in my attempts and finally realized I didn’t need it. Very good complementary advice!