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
@mfikes suggested
^:const
bools as a solution: https://gist.github.com/mfikes/93cb77921ee35c2328b92af441ee5393However, this approach can't be used to elide the spec namespace include, which adds 8 kB in
:advanced
optimizations.(summarized from clojurians slack discussion for posterity)