Focus on spec: Combining Specs with `and`

Clojure's new spec library provides the means to specify the structure of data and functions that take and return data. In this series, we'll take one Clojure spec feature at a time and examine it in more detail than you can find in the spec guide.

In our last post, we explored the simplest specs: predicate functions and sets. In this post we'll look at how you can start to combine specs using the and spec.

For example, we can create a spec that covers the notion of a large even fixed-precision integer with:

(s/valid? (s/and int? even? #(> % 1000)) :foo)  ;; false
(s/valid? (s/and int? even? #(> % 1000)) 10)    ;; false
(s/valid? (s/and int? even? #(> % 1000)) 99999) ;; false
(s/valid? (s/and int? even? #(> % 1000)) 10000) ;; true

For invalid values, spec can also explain to you more precisely why the failure occurred:

(s/explain (s/and int? even? #(> % 1000)) :foo)
;; val: :foo fails predicate: int?
(s/explain (s/and int? even? #(> % 1000)) 10)
;; val: 10 fails predicate: (> % 1000)
(s/explain (s/and int? even? #(> % 1000)) 99999)
;; val: 99999 fails predicate: even?

If we examine a conformed value of this spec, we see that the original value is returned:

(s/conform (s/and int? even? #(> % 1000)) 1234)
;;=> 1234

We will revisit how conformed values flow through s/and specs in a future post as this is not necessarily always the case.

s/and specs also automatically act as a generator that satisfies all the predicates:

(take 5 (s/exercise (s/and int? even? #(> % 1000))))
;;=> ([1814 1814] [1422 1422] [364620 364620] [58602 58602] [4830 4830])

When s/and creates a generator it starts with the first spec (here int?) and generates values:

(take 5 (s/exercise int?))
;;=> ([0 0] [-1 -1] [-2 -2] [1 1] [2 2])

The first spec in an s/and must be able to act as a generator, either due to a built-in mapping or because a custom generator was supplied with it. Most "type" related predicates in Clojure have associated generators and are good choices. Starting an s/and with a predicate like even? would presume too much:

(s/exercise (s/and even? #(> % 1000)))
;; Unable to construct gen at: [] for: even?

Clojure supports many kinds of numbers and even? is a valid question to ask of both fixed and arbitrary precision integers (other predicates like pos? are supported on even more number domains). If even? were to have a generator, what kind of values would it generate? even? shouldn't have to make that decision. It must be combined with another predicate that generates values to produce a valid compound generator.

Values generated from the int? predicate are then passed to the next predicate (even?) and only values that satisfy that predicate are kept:

(take 5 (s/exercise (s/and int? even?)))
;;=> ([0 0] [0 0] [0 0] [-2 -2] [-2 -2])

Finally those values are passed to the final predicate (#(> % 1000)) and only values satisfying that predicate are kept:

(take 5 (s/exercise (s/and int? even? #(> % 1000))))
;;=> ([1620 1620] [1305182 1305182] [3180 3180] [465456 465456] [1198 1198])

In this example the compound generator works because the initial and subsequent domains were relatively "dense" with acceptable values for the next predicate.

Consider this much pickier predicate instead:

(require '[clojure.string :as str])
(s/exercise (s/and string? #(str/includes? % "hi")))
;; Couldn't satisfy such-that predicate after 100 tries.

While the generator would eventually produce a string here or there that includes the substring "hi", the space of acceptable values is much sparser than our prior examples. Fortunately, spec also allows us to supply custom generators using s/with-gen or to override generators by path or name at the point of use when using them in places like clojure.spec.test/instrument and clojure.spec.test/check. Creating custom generators will be the subject of future posts in the series.

To learn more about spec, check out the spec guide or stay tuned for more in this blog series!

Get In Touch