Jim Cheung

reading notes on Programming Clojure 3rd edition

Chapter 5. Specifications

(require '[clojure.spec.alpha :as s])

Predicates

(s/def ::company-name string?)
(s/valid? ::company-name 10)

Enumerated values

(s/def :marble/color #{:red :green :blue})
(s/valid? :marble/color :yellow)
(s/def :bowling/roll #{0 1 2 3 4 5 6 7 8 9 10})
(s/valid? :bowling/roll 15)

Range Specs

(s/def :bowling/ranged-roll (s/int-in 0 11))
(s/valid? :bowling/ranged-roll 10)

Handling nil

(s/def ::company-name-2 (s/nilable string?))
(s/valid? ::company-name-2 nil)

Logical Specs

(s/def ::odd-int (s/and int? odd?))
(s/valid? ::odd-int 5)
(s/valid? ::odd-int 10)

(s/def ::odd-or-42 (s/or :odd ::odd-int :42 #{42}))

(s/explain ::odd-or-42 8)

Collection Specs

The two most common collection specs you’ll use are s/coll-of and s/map-of.

(s/def ::names (s/coll-of string?))
(s/valid? ::names ["Alex" "Stu"])
(s/valid? ::names #{"Alex" "Stu"})
(s/valid? ::names '("Alex" "Stu"))

(s/def ::my-set (s/coll-of int? :kind set? :min-count 2))
(s/valid? ::my-set #{10})
(s/explain ::my-set #{10})

The s/coll-of spec also comes with many additional options supplied as keyword arguments at the end of the spec.

Collection Sampling

The sampling collection specs are s/every and s/every-kv for collections and maps, respectively. They are similar in operation to s/coll-of and s/map-of, except they check up to only s/*coll-check-limit* elements (by default, 101).

Tuples

(s/def ::point (s/tuple float? float?))
(s/conform ::point [1.3 2.7])

Information Maps

{::music/id #uuid "40e30dc1-55ac-33e1-85d3-1f1508140bfc"
 ::music/artist "Rush"
 ::music/title "Moving Pictures"
 ::music/date #inst "1981-02-12"}

(s/def ::music/id uuid?)
(s/def ::music/artist string?)
(s/def ::music/title string?)
(s/def ::music/date inst?)

To specify a map of attributes, we use s/keys, which describes both required and optional attributes:

(s/def ::music/release
  (s/keys :req [::music/id]
          :opt [::music/artist
                ::music/title
                ::music/date]))

Sequences With Structure

the most common regex op spec is s/cat

(s/def ::cat-example (s/cat :s string? :i int?))
(s/valid? ::cat-example ["abc" 100])

(s/conform ::cat-example ["abc" 100])

There is also a regex op spec s/alt for indicating alternatives within the sequential structure.

(s/def ::alt-example (s/alt :i int? :k keyword?))
(s/valid? ::alt-example [100])
(s/valid? ::alt-example [:foo])
(s/conform ::alt-example [:foo])

Repetition Operators

There are three repetition operators – s/? for 0 or 1, s/* for 0 or more, and s/+ for 1 or more.

(s/def ::oe (s/cat :odds (s/+ odd?) :even (s/? even?)))
(s/conform ::oe [1 3 5 100])

named regex ops, which allows us to factor regex op specs into smaller reusable pieces:

(s/def ::odds (s/+ odd?))
(s/def ::optional-even (s/? even?))
(s/def ::oe2 (s/cat :odds ::odds :even ::optional-even))
(s/conform ::oe2 [1 3 5 100])

Variable Argument Lists

(s/def ::println-args (s/* any?))

spec the arguments to intersection as follows

(s/def ::intersection-args
  (s/cat :s1 set?
         :sets (s/* set?)))

(s/conform ::intersection-args '[#{1 2} #{2 3} #{2 5}])

In this case, because each argument is the same spec, we could also use just s/+:

(s/def ::intersection-args-2 (s/+ set?))
(s/conform ::intersection-args-2 '[#{1 2} #{2 3} #{2 5}])

You can spec atom’s args like this

(s/def ::meta map?)
(s/def ::validator ifn?)
(s/def ::atom-args
  (s/cat :x any? :options (s/keys* :opt-un [::meta ::validator])))

(s/conform ::atom-args [100 :meta {:foo 1} :validator int?])

Multi-arity Argument Lists

multi-arity argument lists in the repeat function

(s/def ::repeat-args
  (s/cat :n (s/? int?) :x any?))
(s/conform ::repeat-args [100 "foo"])
(s/conform ::repeat-args ["foo"])

Specifying Functions

Function specs are a combination of three different specs for the arguments, the return value, and the “fn” spec that describes the relationship between the arguments and return.

Let’s start with a function spec for rand:

(s/def ::rand-args (s/cat :n (s/? number?)))
(s/def ::rand-ret double?)
(s/def ::rand-fn
  (fn [{:keys [args ret]}]
    (let [n (or (:n args) 1)]
      (cond (zero? n) (zero? ret)
            (pos? n) (and (>= ret 0) (< ret n))
            (neg? n) (and (<= ret 0) (> ret n))))))

(s/fdef clojure.core/rand
  :args ::rand-args
  :ret ::rand-ret
  :fn ::rand-fn)

Anonymous Functions

Use s/fspec to define the spec of an anonymous function. The syntax is the same as s/fdef but omits the function name.

For instance, consider a function opposite

(defn opposite [pred]
  (comp not pred))

(s/def ::pred
  (s/fspec :args (s/cat :x any?)
           :ret boolean?))

(s/fdef opposite
  :args (s/cat :pred ::pred)
  :ret ::pred)

Generative Function Testing

Spec implements automated generative testing with the function check in the namespace clojure.spec.test.alpha (commonly aliased as stest)

Let’s see how it works with a spec for the Clojure core function symbol

(s/fdef clojure.core/symbol
  :args (s/cat :ns (s/? string?) :name string?)
  :reg symbol?
  :fn (fn [{:keys [args ret]}]
        (and (= (name ret) (:name args))
             (= (namespace ret) (:ns args)))))

And then we can run the test as follows:

(require '[clojure.spec.test.alpha :as stest])
(stest/check 'clojure.core/symbol)

Generating Examples

The argument spec we used above was (s/cat :ns (s/? string?) :name string?). To simulate how check generates random arguments from that spec, we can use the s/exercise function, which produces pairs of examples and their conformed values:

(s/exercise (s/cat :ns (s/? string?) :name string?))

Combining Generators With s/and

For example, consider the following spec for an odd number greater than 100:

(defn big? [x] (> x 100))

(s/def ::big-odd (s/and odd? big?))

This would work as a spec, but its automatic generator doesn’t work:

(s/exercise ::big-odd)

The problem is that while many common Clojure predicates have automatically mapped generators, the predicates we’re using here do not. The odd? predicate works on more than one numeric type and so is not mapped to a generator. The big? predicate is a custom predicate that will never have mappings.

To fix this, we need to add an initial predicate that has a mapped generator—the type-oriented predicates are all good choices for that. Let’s insert int? at the beginning:

(s/def ::big-odd-int (s/and int? odd? big?))

(s/exercise ::big-odd-int)

When you debug generators for s/and specs, remember that only the first component spec’s generator is used.

Creating Custom Generators

We can create generators in several ways. The simplest way is to first create a different spec, then use s/gen to retrieve its generator. Alternately, the clojure.spec.gen.alpha namespace, typically aliased as gen, contains other generators and functions to combine generators.

gen/fmap allows you to start from a source generator, then modify each generated value by applying another function.

(require '[clojure.string :as str])
(require '[clojure.spec.gen.alpha :as gen])

(s/def ::sku
  (s/with-gen (s/and string? #(str/starts-with? % "SKU-"))
    (fn [] (gen/fmap #(str "SKU-" %) (s/gen string?)))))

(s/exercise ::sku)