I'm modeling a state machine in ClojureScript and am using clojure.spec to check invariants in each state.
However, I'd like to remove this checking in the production build, since it adds ~ 200kB minified code size and 100ms startup time.
I haven't been able to find a tidy way to do this --- this writeup explains the approaches I've considered and their tradeoffs.
Here's the current setup:
(ns app.state-machines.foo
(:require [clojure.spec.alpha :as s]))
(defn new-machine
[opts]
[{:state/name "A"
:state/spec (s/keys :req [::foo ::bar])}
{:state/name "B"
:state/spec (s/keys :req [::baz])}
...])
(s/def ::foo ...)
(s/def ::bar ...)
(s/def ::baz ...)A few notes / constraints:
-
The state machine is just a data structure, which must be parameterized by arguments (
opts). -
The specs for each state are defined together with the state itself. This is for usability reasons.
-
All specs are namespaced together with the state machine definition itself.
Potential solutions I've considered:
The reader's :features option (docs) suggests that I may be able use conditional reading to write something like:
(ns app.state-machines.foo
(:require #?(:dev-mode [clojure.spec.alpha :as s])))
(defn new-machine
[opts]
[{:state/name "A"
#?@(:dev-mode [:state/spec (s/keys :req [::foo ::bar])])}
{:state/name "B"
#?@(:dev-mode [:state/spec (s/keys :req [::baz])])}
...])
#?(:dev-mode (do
(s/def ::foo ...)
(s/def ::bar ...)
(s/def ::baz ...)))It seems like this would solve the issue nicely, but I'm not sure how to pass the :dev-mode flag to the cljs compiler or the clojure compiler itself.
I've never seen this use of reader conditionals in the wild, which makes me think it's not possible and/or highly frowned upon by the community.
If it's impossible to do reader conditionals, a similar approach could be made with a macro that checks a var (dev-mode?) at macroexpansion time
However, this sounds like a brittle nightmare.
It's possible to split the namespace up into parts: app.state-machines.foo would have no references to spec, and then app.state-machines.foo-spec would have the annotations.
Then different builds could have different ClojureScript compiler :main options, where the "dev-main" would require all app.state-machines.foo-spec but "production-main" wouldn't.
However, the big downside to this approach (aside from proliferation of files), is that it separates the specs from what they're defining, which makes for a much worse programmer experience. I'd really like to avoid this.
If you have an idea on how to solve this, I'd love to hear from you! Please leave a comment below.
Thanks!
Kevin
I added support for the custom reader features in shadow-cljs as of version
2.0.74.The use-case I envisioned this for was for removing some code from compilation when targeting
nodevs thebrowser. There were somensrequires that could not be loaded (and should not be loaded) when running innode. Since conditional requires were otherwise not possible this seems like a reasonable solution.shadow-cljsalready has a built-in concept for:devand:releasemode but does not yet automatically transfer those to the:reader-features. You could however do something like{:builds {:app {:target :browser ... ;; build config here :dev {:compiler-options {:reader-features #{:dev-mode}}}}}This would only use the
:reader-featuresin:devbuilds,shadow-cljs release appwould not use them.Until CLJS-2396 resolves this only works in
shadow-cljsthough.