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 .
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])
fromA.cljc
. I’m sure I also copy-pasted the helper function inA.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 inM.cljc
, and(:require [M])
fromA.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