clojure.spec for functions by example
Clojure.spec for functions by example: their inputs, outputs, and the relationships between them. Updated 9/6/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?))
Note: with s/cat
the function’s argument names and the keywords used to describe them in the spec don’t have to match — it’s the position that’s important.
Function argument lists can be spec’d as a sequence, which makes perfect sense as they are a sequence of arguments. I often prefer s/cat
, treating the arguments as a tagged concatenation, although :args
specs can also be described with s/tuple
, s/alt
, s/+
, s/*
, s/?
, etc.
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 when & {:keys [who which what]}]
(format "%s %s %s %s %s" why when who which what))
(s/fdef big-fun
:args (s/cat :why string?
:when #(instance? LocalDateTime %)
:kwargs (s/keys* :req-un [(or ::who
(and ::which ::what))])))
(stest/instrument `big-fun)
(big-fun "because" (LocalDateTime/now) :which 1 :what 4) ;; valid
(big-fun "said so" (LocalDateTime/now) :who "I") ;; also valid
(big-fun "because" (LocalDateTime/now) :which 1) ;; invalid
Here we defined a kwargs spec that requires either a :who
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.
And here’s a variadic function with positional destructuring:
(defn slarp [path & [blurp? glurf?]]
(str path blurp? glurf?))
(s/fdef slarp
:args (s/cat :path string?
:rest (s/? (s/cat :blurp? int?
:glurf? boolean?))))
(s/exercise-fn `slarp)
=>
([("") ""]
[("" -1 false) "-1false"]
[("e7" 0 true) "e70true"]
[("du9") "du9"]
[("K1" -6 false) "K1-6false"]
[("op0" -3 false) "op0-3false"]
[("2y" 2 false) "2y2false"]
[("qaGWVK") "qaGWVK"]
[("9h1Rh") "9h1Rh"]
[("") ""])
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:
- 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.
- The returned elements should be a subset of the input elements.
Warning
Any function g
you pass into a higher-order, instrumented function f
may be invoked multiple times as spec tests its conformance! This might be a problem if g
is expensive or has side effects.
(defn f [g] (g 1))
(s/fdef f :args (s/cat :g (s/fspec :args (s/tuple int?))))
(st/instrument `f)
Now if we call f
with a side-effecting function (printing text) we can see the given function invoked many times with random inputs — a sort of miniature quick-check.
(f #(doto % prn inc))
-1
0
-2
...
59
-69770
-1165
1
=> 1
Possible Workarounds
Simplify the higher-order function’s spec:
(s/fdef f :args (s/cat :g ifn?))
Or disable the behavior using a dynamic binding in spec:
(binding [clojure.spec.alpha/*fspec-iterations* 0]
(f #(doto % prn inc)))
1
=> 1
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.