Clojure.spec for functions by example: their inputs, outputs, and the relationships between them. Updated 3/26/2018.

Simple Input & Output

We have a simple function that takes a single argument and returns a set:

(defn digits
  "Takes just an int and returns the set of its digit characters."
  [just-an-int]
  (into #{} (str just-an-int)))

Function specs are typically defined with s/fdef. Let’s provide a spec for the arguments:

(s/fdef digits :args (s/cat :just-an-int int?))

Function argument lists can be spec’d with s/cat, treating the arguments as a tagged concatenation or sequence. (If you think about it, defn function arguments are just a vector, man.) Here we only have one argument and we want it to be an integer. Note that the function’s argument names and the keywords used to describe them in the s/cat spec do not have to match, though they do in this example.

We can now instrument the function to assert calls against its :args spec:

(stest/instrument `digits)

This only covers the function’s inputs. We can also spec the return value:

(s/fdef digits
        :args (s/cat :just-an-int int?)
        :ret (s/coll-of char? :kind set? :min-count 1))

With a :ret spec, we can now check the function against our spec with generative tests:

(stest/check `digits)

Keyword Arguments

There’s a s/keys* macro that works for kwargs, similarly to how s/keys works for maps.

(defn big-fun
  [why where & {:keys [which-what which what]}]
  (format "%s %s %s %s %s" why where which-what which what))

(s/fdef big-fun
        :args (s/cat :why #(instance? LocalDateTime %)
                     :where string?
                     :kwargs (s/keys* :req-un [(or ::which-what
                                                   (and ::which ::what))])))

(stest/instrument `big-fun)
(big-fun (LocalDateTime/now) "My Place" :which 1 :what 4) ;; valid
(big-fun (LocalDateTime/now) "My Place" :which-what nil)  ;; also valid

Here we defined a kwargs spec that requires either a :which-what key or a :which key and :what key, just like we could’ve done for a map.

Multi-arity & Variadic Functions

We can spec multi-arity functions using s/alt to define the alternate cases, and variadic functions using s/*. Here’s a function with two fixed arities and one variadic arity:

(defn such-arity
  ([] "nullary")
  ([one] "unary")
  ([one two & many] "one two many"))

(s/fdef such-arity
        :args (s/alt :nullary (s/cat)
                     :unary (s/cat :one any?)
                     :variadic (s/cat :one any?
                                      :two any?
                                      :many (s/* any?))))

We can spec the zero-arity with (s/cat); no arguments. We specify the variadic arguments with s/*. The “regex” specs compose to match a single sequence. This composition of s/cat and s/* produces a spec that wants a sequence of two arguments and then any number of additional arguments.

Higher-Order Functions

This is an only slightly different, more involved example than the one in the clojure.spec guide.

Here’s a spec for a function that takes any two arguments and returns one of them:

(s/def ::pick-fn
  (s/with-gen
    (s/fspec :args (s/cat :a any? :b any?)
             :ret any?
             :fn #(or (= (:ret %) (:a (:args %)))
                      (= (:ret %) (:b (:args %)))))
    #(gen/return (fn [a b] (rand-nth [a b])))))

This function spec is registered to a qualified keyword rather than a function symbol via s/fdef. This is defined with s/def and s/fspec because I want to use this function spec in more than one place, and specify a custom generator. Notice the additional :fn spec that asserts the returned value is one of the input values. This spec will be checked when we check the function with generative tests.

I use s/with-gen to specify a custom generator that returns a function that returns one of its arguments randomly. I could however just allow spec to create a function generator from my function spec; the generated functions would return random samples of its :ret spec, and any calls would have their arguments asserted against its :args spec. A spec-generated function would not be desirable in this contrived example because we want to ensure the function always returns something it was given.

How about this for some implementation of ::pick-fn:

(defn battle
  "Returns the victor."
  [contestant-1 contestant-2]
  (if (even? (System/nanoTime)) contestant-1 contestant-2))

Now I want to associate the ::pick-fn spec with battle function. We can achieve the same effect as (s/fdef battle ...) with s/def:

(s/def battle ::pick-fn)
(stest/instrument `battle)
(stest/check `battle)

Now let’s write a higher-order function to take a ::pick-fn-conformant function and a collection:

(defn winners
  "Uses f to pick a successor between each element of coll, returning sequence of successors."
  [f coll]
  (rest (reductions f coll)))
(winners battle (range 20))
;;=> (0 0 0 0 4 4 6 6 6 6 10 10 10 10 14 14 14 14 14 19)

And a spec for the function:

(s/fdef winners
        :args (s/cat :f ::pick-fn
                     :coll (s/coll-of any? :min-count 2))
        :ret coll?
        :fn (fn [{:keys [args ret]}]
              (and (= (count ret) (dec (count (:coll args))))
                   (set/subset? (set ret) (set (:coll args))))))
(stest/instrument `winners)
(stest/check `winners)

The winners function has some odd invariants. The f arg must conform to ::pick-fn and coll must have at least two elements. Additionally, the relationship between inputs and outputs for winners is expressed in the :fn spec:

  1. It should always return a collection that has one less element than the input collection. This is because the first “pick” is for the first two input elements.
  2. The returned elements should be a subset of the input elements.

Run-time Checking

Instrumentation

You can call instrument with no arguments to instrument all loaded/instrumentable vars, and you may want to do this at dev/test time but not in production to avoid performance penalties. One convenient solution is to have a separate dev/test entrypoint in your program that calls instrument after the relevant namespaces are loaded. Another option with Leinengen is to use :injections in your project.clj under specific profile(s):

:injections [(require 'lib.core) ;; all instrumented fns should be loaded here
             (require 'clojure.spec.test.alpha)
             (clojure.spec.test.alpha/instrument)]

Spec’s instrument only checks function inputs (:args specs). Alternatively, the Orchestra library has a drop-in replacement that also checks :ret and :fn specs.

:pre and :post checks

This will throw an AssertionError if called with any non-nil, non-string value:

(defn stringer-bell
  "Prints a string and rings bell."
  [s]
  {:pre [(s/valid? (s/nilable string?) s)]}
  (println s "\007"))

Asserting

There’s also s/assert which can be used in function bodies:

(defn stringer-bell [s]
  (s/assert (s/nilable string?) s)
  (println s "\007"))

You need to enable spec’s off-by-default assertion checking for this to have an effect:

(s/check-asserts true)

s/assert will throw a more detailed exception than a :pre/:post condition, although be careful not to use s/assert there: it returns valid values and if the spec allows nil, the condition will fail when a valid nil is passed.