Skip to content

Instantly share code, notes, and snippets.

@lynaghk
Last active November 13, 2017 10:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lynaghk/0608a43f173558b6fcf30c3be53d77dd to your computer and use it in GitHub Desktop.
Save lynaghk/0608a43f173558b6fcf30c3be53d77dd to your computer and use it in GitHub Desktop.
cljs production mode

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:

  1. The state machine is just a data structure, which must be parameterized by arguments (opts).

  2. The specs for each state are defined together with the state itself. This is for usability reasons.

  3. All specs are namespaced together with the state machine definition itself.

Potential solutions I've considered:

Introducing a new reader conditional

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.

Code walking macro

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.

Slicing up the namespace

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.

Other?

If you have an idea on how to solve this, I'd love to hear from you! Please leave a comment below.

Thanks!

Kevin

@lynaghk
Copy link
Author

lynaghk commented Nov 12, 2017

@mfikes suggested ^:const bools as a solution: https://gist.github.com/mfikes/93cb77921ee35c2328b92af441ee5393

However, 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)

@thheller
Copy link

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 node vs the browser. There were some ns requires that could not be loaded (and should not be loaded) when running in node. Since conditional requires were otherwise not possible this seems like a reasonable solution.

shadow-cljs already has a built-in concept for :dev and :release mode 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-features in :dev builds, shadow-cljs release app would not use them.

Until CLJS-2396 resolves this only works in shadow-cljs though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment