Search code examples
clojureclojure.spec

Clojure spec: Using a "spec" instead of a "pred" in "coll-of" actually works. Is this ok?


I'm trying out Clojure spec and calling it from a function :pre constraint.

My spec ::mustbe-seql-of-vec-of-2-int shall check whether the argument passed to the function is a sequential of anything from 0 to 4 vectors of exactly 2 integers:

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

(s/def ::mustbe-seql-of-vec-of-2-int
   (s/and 
      ::justprint
      (s/coll-of 
         ::mustbe-vec-of-2-int 
         :kind sequential?
         :min-count 0
         :max-count 4)))

So the spec is composed of other specs, in particular ::justprint which does nothing except print the passed argument for debugging and coll-of to test the collection argument.

The doc for coll-of says:

Usage: (coll-of pred & opts)

Returns a spec for a collection of items satisfying pred. Unlike 'every', coll-of will exhaustively conform every value.

However, as first argument, I'm not using a pred (a function taking the argument-to-check and returning a truthy value) but another spec (a function taking the argument to check and returning I'm not sure what), in this case, the spec registered under ::mustbe-vec-of-2-int.

This works perfectly well.

Is this correct style and expected to work?

P.S.

(s/def ::justprint
   #(do 
      ; "vec" to realize the LazySeq, which is not realized by join
      (print (d/join [ "::justprint ▶ " (vec %) "\n"] ))      
      true))

(s/def ::mustbe-vec-of-2-int
   (s/coll-of integer? :kind vector? :count 2))

Solution

  • Is this correct style and expected to work?

    Yes. Specs, keywords that can be resolved to specs, and plain predicate functions can be used interchangeably in many parts of the clojure.spec API. "Pred" in the context of that docstring has a broader meaning than a general Clojure predicate function e.g. nil?.

    (require '[clojure.spec.alpha :as s])
    
    (s/conform int? 1) ;; => 1
    (s/conform int? false) ;; => :clojure.spec.alpha/invalid
    

    s/def modifies the clojure.spec registry, associating the keyword with the spec value. When you pass ::some-spec-keyword to clojure.spec API it resolves the spec values themselves from the registry. You can also "alias" specs e.g. (s/def ::foo ::bar).

    (s/def ::even-int? (s/and int? even?))
    (s/conform ::even-int? 2) ;; => 2
    (s/conform ::even-int? 3) ;; => :clojure.spec.alpha/invalid
    

    So these are all equivalent:

    (s/conform (s/coll-of (s/and int? even?)) [2 4 6 8])
    (s/conform (s/coll-of ::even-int?) [2 4 6 8])
    (s/def ::coll-of-even-ints? (s/coll-of ::even-int?))
    (s/conform ::coll-of-even-ints? [2 4 6 8])