clojure

clojure.spec

Syntax#

  • :: is a shorthand a namespace-qualified keyword. E.g. if we are in the namespace user: ::foo is a shorthand for :user/foo
  • #: or # - map-literal syntax for qualifying keys in a map by a namespace

Remarks#

Clojure spec is a new specification/contracts library for clojure available as of version 1.9.

Specs are leveraged in a number of ways including being included in documentation, data validation, generating data for testing and more.

Using a predicate as a spec

Any predicate function can be used as a spec. Here’s a simple example:

(clojure.spec/valid? odd? 1)
;;=> true

(clojure.spec/valid? odd? 2)
;;=> false

the valid? function will take a spec and a value and return true if the value conforms to the spec and false otherwise.

One other interesting predicate is set membership:

(s/valid? #{:red :green :blue} :red) 
;;=> true

fdef: writing a spec for a function

Let’s say we have the following function:

(defn nat-num-count [nums] (count (remove neg? nums)))

We can write a spec for this function by defining a function spec of the same name:

(clojure.spec/fdef nat-num-count
        :args (s/cat :nums (s/coll-of number?))
        :ret integer?
        :fn #(<= (:ret %) (-> % :args :nums count)))

:args takes a regex spec which describes the sequence of arguments by a keyword label corresponding to the argument name and a corresponding spec. The reason the spec required by :args is a regex spec is to support multiple arities for a function. :ret specifies a spec for the return value of the function.

:fn is a spec which constrains the relationship between the :args and the :ret. It is used as a property when run through test.check. It is called with a single argument: a map with two keys: :args (the conformed arguments to the function) and :ret (the function’s conformed return value).

Registering a spec

In addition to predicates functioning as specs, you can register a spec globally using clojure.spec/def. def requires that a spec being registered is named by a namespace-qualified keyword:

(clojure.spec/def ::odd-nums odd?)
;;=> :user/odd-nums

(clojure.spec/valid? ::odd-nums 1)
;;=> true
(clojure.spec/valid? ::odd-nums 2)
;;=> false

Once registered, a spec can be referenced globally anywhere in a Clojure program.

The ::odd-nums syntax is a shorthand for :user/odd-nums, assuming we are in the user namespace. :: will qualify the symbol it precedes with the current namesapce.

Rather than pass in the predicate, we can pass in the spec name to valid?, and it will work the same way.

clojure.spec/and & clojure.spec/or

clojure.spec/and & clojure.spec/or can be used to create more complex specs, using multiple specs or predicates:

(clojure.spec/def ::pos-odd (clojure.spec/and odd? pos?))

(clojure.spec/valid? ::pos-odd 1)
;;=> true

(clojure.spec/valid? ::pos-odd -3)
;;=> false

or works similarly, with one significant difference. When defining an or spec, you must tag each possible branch with a keyword. This is used in order to provide specific branches which fail in error messages:

(clojure.spec/def ::big-or-small (clojure.spec/or :small #(< % 10) :big #(> % 100)))

(clojure.spec/valid? ::big-or-small 1)
;;=> true

(clojure.spec/valid? ::big-or-small 150)
;;=> true

(clojure.spec/valid? ::big-or-small 20)
;;=> false

When conforming a spec using or, the applicable spec will be returned which made the value conform:

(clojure.spec/conform ::big-or-small 5)
;; => [:small 5]

Record specs

You can spec a record as follows:

(clojure.spec/def ::name string?)
(clojure.spec/def ::age pos-int?)
(clojure.spec/def ::occupation string?)

(defrecord Person [name age occupation])

(clojure.spec/def ::person (clojure.spec/keys :req-un [::name ::age ::occupation]))

(clojure.spec/valid? ::person (->Person "john doe" 25 "programmer"))
;;=> true

(clojure.spec/valid? ::person (->Person "john doe" "25" "programmer"))
;;=> false

At some point in the future, a reader syntax or built-in support for qualifying record keys by the records’ namespace may be introduced. This support already exists for maps.

Map specs

You can spec a map by specifying which keys should be present in the map:

(clojure.spec/def ::name string?)
(clojure.spec/def ::age pos-int?)
(clojure.spec/def ::occupation string?)

(clojure.spec/def ::person (clojure.spec/keys :req [::name ::age ::occupation]))

(clojure.spec/valid? ::person {::name "john" ::age 25 ::occupation "programmer"})
;; => true

:req is a vector of keys required to be present in the map. You can specify additional options such as :opt, a vector of keys which are optional.

The examples so far require that the keys in the name are namespace-qualified. But it’s common for map keys to be unqualified. For this case, clojure.spec provides :req and :opt equivalents for unqualified keys: :req-un and :opt-un. Here’s the same example, with unqualified keys:

(clojure.spec/def ::name string?)
(clojure.spec/def ::age pos-int?)
(clojure.spec/def ::occupation string?)

(clojure.spec/def ::person (clojure.spec/keys :req-un [::name ::age ::occupation]))

(clojure.spec/valid? ::person {:name "john" :age 25 :occupation "programmer"})
;; => true

Notice how the specs provided in the :req-un vector as still qualified. clojure.spec, will automatically confirm the unqualified versions in the map when conforming the values.

namespace map literal syntax allows you to qualify all the keys of a map by a single namespace succinctly. For example:

(clojure.spec/def ::name string?)
(clojure.spec/def ::age pos-int?)
(clojure.spec/def ::occupation string?)

(clojure.spec/def ::person (clojure.spec/keys :req [::name ::age ::occupation]))

(clojure.spec/valid? ::person #:user{:name "john" :age 25 :occupation "programmer"})
;;=> true

Notice the special #: reader syntax. We follow this with the namespace we wish to qualify all the map keys by. These will then be checked against the specs corresponding to the provided namespace.

Collections

You can spec collections in a number of ways. coll-of allows you to spec collections and provide some additional constraints. Here’s a simple example:

(clojure.spec/valid? (clojure.spec/coll-of int?) [1 2 3])
;; => true

(clojure.spec/valid? (clojure.spec/coll-of int?) '(1 2 3))
;; => true

Constraint options follow the main spec/predicate for the collection. You can constrain the collection type with :kind like this:

(clojure.spec/valid? (clojure.spec/coll-of int? :kind vector?) [1 2 3])
;; => true

(clojure.spec/valid? (clojure.spec/coll-of int? :kind vector?) '(1 2 3))
;; => false

The above is false because the collection passed in is not a vector.

(clojure.spec/valid? (clojure.spec/coll-of int? :kind list?) '(1 2 3))
;; => true

(clojure.spec/valid? (clojure.spec/coll-of int? :kind set?) #{1 2 3})
;; => true

(clojure.spec/valid? (clojure.spec/coll-of int? :kind set?) #{1 "2" 3})
;; => false

The above is false because not all elements in the set are ints.

You can also constrain the size of the collection in a few ways:

(clojure.spec/valid? (clojure.spec/coll-of int? :kind vector? :count 3) [1 2 3])
;; => true

    (clojure.spec/valid? (clojure.spec/coll-of int? :kind vector? :count 3) [1 2])
;; => false

(clojure.spec/valid? (clojure.spec/coll-of int? :min-count 3 :max-count 5) [1 2 3])
;; => true

    (clojure.spec/valid? (clojure.spec/coll-of int? :min-count 3 :max-count 5) [1 2])
;; => false

You can also enforce uniqueness of the elements in the collection with :distinct:

(clojure.spec/valid? (clojure.spec/coll-of int? :distinct true) [1 2])
;; => true

(clojure.spec/valid? (clojure.spec/coll-of int? :distinct true) [2 2])
;; => false

coll-of ensures all elements in a sequence are checked. For large collections, this can be very inefficient. every behaves just like coll-of, except it only samples a relatively small number of the sequences’ elements from for conformance. This works well for large collections. Here’s an example:

(clojure.spec/valid? (clojure.spec/every int? :distinct true) [1 2 3 4 5])
;; => true

map-of is similar to coll-of, but for maps. Since maps have both keys and values, you must supply both a spec for the key and a spec for the value:

(clojure.spec/valid? (clojure.spec/map-of keyword? string?) {:red "red" :green "green"})
;; => true

Like coll-of, map-of checks conformance of all map key/values. For large maps this will be inefficient. Like coll-of, map-of supplies every-kv for efficiently sampling a relatively small number of values from a large map:

(clojure.spec/valid? (clojure.spec/every-kv keyword? string?) {:red "red" :green "green"})
;; => true

Sequences

spec can describe and be used with arbitrary sequences. It supports this via a number of regex spec operations.

(clojure.spec/valid? (clojure.spec/cat :text string? :int int?) ["test" 1])
;;=> true

cat requires labels for each spec used to describe the sequence. cat describes a sequence of elements and a spec for each one.

alt is used to choose among a number of possible specs for a given element in a sequence. For example:

(clojure.spec/valid? (clojure.spec/cat :text-or-int (clojure.spec/alt :text string? :int int?)) ["test"])
;;=> true

alt also requires that each spec is labeled by a keyword.

Regex sequences can be composed in some very interesting and powerful ways to create arbitrarily complex sequence-describing specs. Here’s a slightly more complex example:

(clojure.spec/def ::complex-seq (clojure.spec/+ (clojure.spec/cat :num int? :foo-map (clojure.spec/map-of keyword? int?))))
(clojure.spec/valid? ::complex-seq [0 {:foo 3 :baz 1} 4 {:foo 4}])
;;=> true

Here ::complex-seq will validate a sequence of one or more pairs of elements, the first being an int and the second being a map of keyword to int.


This modified text is an extract of the original Stack Overflow Documentation created by the contributors and released under CC BY-SA 3.0 This website is not affiliated with Stack Overflow