Focus on spec: Predicates

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.

The simplest form of spec is a predicate, which is a Clojure function that takes a single value and responds with a logical value indicating whether the value is valid.

Many existing Clojure core functions can serve as predicates. By convention, predicates in Clojure often end in "?":

  • Values: any? nil? some?
  • Booleans: boolean? false? true?
  • Strings: string? clojure.string/blank?
  • Numbers: int? integer? even? nat-int? neg-int? neg? number? odd? pos-int? pos? ratio? rational? bigdec? decimal? double? zero?
  • Symbols and keywords: symbol? keyword? ident? qualified-ident? qualified-keyword? qualified-symbol? simple-ident? simple-keyword? simple-symbol? special-symbol?
  • Collections: coll? associative? counted? empty? indexed? list? map-entry? map? reversible? seq? seqable? sequential? set? sorted? vector?
  • Functions: fn? ifn?
  • Other: bytes? char? class? future? inst? record? uri? uuid? var?

The spec function s/valid? can be used to check whether a value conforms to a spec:

(require '[clojure.spec :as s])
(s/valid? even? 1000)
;;=> true

spec also has a way to conform a value, returning to you either a value that has passed the validity check or the special value :clojure.spec/invalid. Conforming with a predicate always returns the original value:

(s/conform even? 1000)
;;=> 1000

In future posts we'll see examples where the conformed value of more complex specs returns a structure that reveals how it was conformed.

Many of the Clojure predicates that check value types are also mapped to suitable data generators and can be used to generate sample values. The s/exercise function can be used to generate pairs of sample values and the conformed version of the sample. Again, for predicates these will be the same (results truncated here):

(take 3 (s/exercise boolean?))
;;=> ([false false] [true true] [true true])
(take 5 (s/exercise int?))
;;=> ([0 0] [-1 -1] [-2 -2] [3 3] [7 7])
(take 5 (s/exercise simple-keyword?))
;;=> ([:I :I] [:o:Xs :o:Xs] [:X+:G :X+:G] [:m1:-*6:1:H :m1:-*6:1:H] [:i429- :i429-])
(take 5 (s/exercise vector?))
;;=> ([[] []] [[1/3] [1/3]] [[-1.75 :KI+] [-1.75 :KI+]] [[:D2xK.!.!05/+*!Z] [:D2xK.!.!05/+*!Z]] [[:wij:8] [:wij:8]])

Sets are also valid predicates and are the recommended way to specify a set of enumerated values:

(s/valid? #{:club :diamond :heart :spade} :club) ;; true
(s/valid? #{:club :diamond :heart :spade} 42) ;; false
(s/valid? #{42} 42) ;; true

And they can act as generators:

(take 5 (s/exercise #{:club :diamond :heart :spade}))
;;=> ([:diamond :diamond] [:spade :spade] [:diamond :diamond] [:heart :heart] [:diamond :diamond])

But you're not constrained to just the Clojure core predicates and sets - you can use any function that validates a single value.

(s/valid? #(> % 5) 10)
(s/valid? #(clojure.string/starts-with? % "INV") "INV-1000")
(s/valid? orders/overdue? {:orders/due #inst "2016-09-01"})

Custom functions by themselves will not be able to act as generators, but we'll see ways to combine custom functions with core functions and to create custom generators in future posts.

Most of the Clojure predicates do not include nil as a valid value:

(s/valid? int? 100) ;; true
(s/valid? int? nil) ;; false

spec includes a special function that can be used to add support for nil to an existing predicate:

(s/valid? (s/nilable int?) 100) ;; true
(s/valid? (s/nilable int?) nil) ;; true
(take 5 (s/exercise (s/nilable int?)))
;;=> ([nil nil] [nil nil] [1 1] [1 1] [-7 -7])

spec also includes many other functions for composing specs and we will explore these in future posts.

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