clojure.spec (the library spec.alpha, namespace clojure.spec.alpha, conventionally required as s) describes the structure of data and functions. A spec can validate, conform (parse), explain a failure, generate test data and test functions. It was added with Clojure 1.9 and is a separate library, which the language depends on. Specs are ordinary data and code in .clj, .cljs and .cljc files and need no new syntax.
Predicates and the registry
Any predicate function is a spec. (s/valid? even? 10) is true; (s/conform even? 1000) returns the value if valid, or the keyword :clojure.spec.alpha/invalid. A set is a spec of its members (#{:club :diamond}), and nil?, string? and #(> % 5) work as they are.
(s/def :ns/name spec) adds a spec to the global registry under a namespaced keyword (or a qualified symbol, for functions). A registered name can be used wherever a spec is expected. s/get-spec reads the registry, and s/registry returns it.
(require '[clojure.spec.alpha :as s])
(s/def :order/date inst?)
(s/def :deck/suit #{:club :diamond :heart :spade})
(s/def :num/big-even (s/and int? even? #(> % 1000)))
(s/def :domain/name-or-id (s/or :name string? :id int?))
(s/valid? :num/big-even 100000) ; => true
(s/conform :domain/name-or-id "abc") ; => [:name "abc"]
The spec forms
| Form | Describes |
|---|---|
s/def, s/spec, s/and, s/or, s/nilable, s/merge | register; wrap a predicate; all must hold (each sees the conformed value of the one before); one of tagged alternatives (conforms to [tag value]); a value or nil; the union of map specs |
s/keys :req :opt :req-un :opt-un | an entity map: required and optional qualified keys (:req may use (or …) and (and …)), and unqualified keys that are checked against the specs of the qualified names. Every qualified key in the map that has a registered spec is checked, not only the listed ones |
s/keys* | the same, for key-value pairs in a sequence (keyword arguments) |
s/multi-spec | chooses a spec by a multimethod, for maps that vary by a type key |
s/coll-of s/map-of s/tuple s/every s/every-kv | homogeneous collections (options :kind :count :min-count :max-count :distinct :into); a map with spec'd keys and values; a fixed-length vector with a spec per position; the sampling versions of coll-of and map-of for large collections |
s/cat s/alt s/* s/+ s/? s/& | regex specs for sequences: concatenation with tags ((s/cat :start int? :end int?) conforms to a map), alternatives, zero or more, one or more, zero or one, and a regex followed by predicates. Nested regexes splice into one sequence; wrap in s/spec to nest |
s/fspec, s/fdef | a function spec (:args :ret :fn); and the registration of one for a named function |
s/conformer, s/nonconforming, s/int-in, s/double-in, s/inst-in | a spec with a custom conforming function; ranges of integers, doubles and instants |
s/with-gen, s/gen, s/exercise, s/exercise-fn | attach a generator; get one; sample values with their conformed form |
Validating, conforming and explaining
| Function | Does |
|---|---|
valid? | true or false |
conform, unform | the conformed value (tagged alternatives, named regex parts) or :clojure.spec.alpha/invalid; and the way back |
explain, explain-str, explain-data, explain-out | print why a value fails; return it as a string; return it as data (:clojure.spec.alpha/problems with :path, :pred, :val, :via and :in); the printer, which can be rebound (*explain-out*) |
assert, check-asserts | validate and throw an assertion error, returning the value on success: only when asserts are enabled with (s/check-asserts true) (the default is off), and only in code compiled with *compile-asserts* true |
describe, form | the spec as an abbreviated or complete form |
Specs for functions
(s/fdef name :args spec :ret spec :fn spec) specifies a function's arguments (a regex spec over the argument list, as if called with apply), return value and the relation between them (the :fn spec receives a map of :args, conformed, and :ret). The spec is shown by doc. Because a macro's arguments are forms, fdef can also describe macros.
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
Instrumentation and testing
The namespace clojure.spec.test.alpha (alias stest) works on function specs:
| Function | Does |
|---|---|
instrument, unstrument, with-instrument-disabled | wrap functions so that calls are checked against their :args spec (it takes fully qualified symbols, written with a syntax-quote); not for production. :ret and :fn are not checked by instrumentation |
check | generates arguments from :args, calls the function and checks :ret and :fn (property-based testing) |
checkable-syms, instrumentable-syms, instrumented-vars, enumerate-namespace, summarize-results | list what can be checked or is instrumented; summarise a check run |
Generators come from clojure.spec.gen.alpha, which loads test.check only when it is used, so that library must be on the classpath for gen, exercise and check.
Not described
Known, not yet covered
- Reference: the Spec guide (about 1,350 lines) was read through its headings and the sections on
keys, collections, function specs and instrumentation; the sections onmulti-spec, sequences, macros, the card-game example and generators in detail were not read, and the spec rationale and the library's own docstrings were not tabulated. Clojure spec 2 (clojure.alpha.spec) is not covered. - Reference:
clojure.spec.gen.alphanames (for-all*,quick-check,generateand the lazy wrappers) are not described.
Unknown
- Reference: that
s/defaccepts a qualified symbol for functions, and the details ofs/&ands/everybeyond their documentation strings, are from knowledge of the library; the README says that Clojure 1.9 depends on it and that it is alpha and subject to change.
See also
- Functions and destructuring –
:preand:postconditions - The standard library – the names the viewer colours