<-- home

Automatically Instrumentating Functions With Clojure.spec

Clojure.spec has a way to turn on instrumentation for functions that are spec'd with ~clojure.spec.alpha/fdef~. If you call ~clojure.spec.test.alpha/instrument~ with a symbol name of a function, and that function has a spec attached to it, then each time you call that function in the future clojure will check that the arguments you passed to the functions pass the function spec and show you a spec error message if they don't.

  (require '[clojure.spec.alpha :as s]
           '[clojure.spec.test.alpha :as stest])

  (defn inc+ [n] (inc (inc n)))

  (s/fdef inc+
    :args (s/cat :n number?))

  (stest/instrument `inc+)

  (inc+ "1")

  ;; Spec: #object[...]
  ;; Value: ("1")
  ;; val: "1"
  ;; in: [0]
  ;; failed: number?
  ;; at: [:args :n]

But I've found that while you are developing it's easy to forget to instrument every new function that you attach a spec to

  (defn dec+ [n] (dec (dec n)))

  (s/fdef dec+
    :args (s/cat :n number?)
    :ret number?)

  (dec "1")

  ;; Forgot to instrument this function

  (dec+ "3")

  ;; ClojureCast
  ;; 1. Unhandled java.lang.ClassCastException
  ;; java.lang.String cannot be cast to java.lang.Number

So I wanted to come up with a way to avoid having to manually instrument each function as its spec is defined. I remembered that clojure.spec has an internal registry where it keeps track of specs, and when I looked at the source code I found that there is a private var ~clojure.spec.alpha/registry-ref~ which points to an atom that stores every registered spec in your clojure process. Clojure allows you to attach watches to atoms, which will call a function every time the atom's contents are updated. It was easy enough to create a watch on the ~registry-ref~ var and call a function every time a new spec is registered which will instrument every spec'd function in your process.

  (add-watch (deref #'s/registry-ref) :spec-instrumentation
    (fn [_ _ _ registry]
      (stest/instrument (filter symbol? (keys registry)))))

This function could easily be added to your Leiningen ~profiles.cljs~ in your home directory which will allow you get automatic spec instrumentation in every Leiningen REPL you start, including when you jack-in to a Lein project from CIDER.

  {:user {:repl-options {:init (do
                                 (require '[clojure.spec.alpha :as s])
                                 (require '[clojure.spec.test.alpha :as stest])
                                 (add-watch (deref #'s/registry-ref) :spec-instrumentation
                                   (fn [_ _ _ registry]
                                     (stest/instrument (filter symbol? (keys registry))))))}}}

- Adam Frey, May 2018

<-- home