Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A Clojure Spec that matches and generates an ordered vector of variable length

Lets start out with the regular sequence of

(require '[clojure.spec     :as spec]
         '[clojure.spec.gen :as gen])
(spec/def ::cat (spec/cat :sym symbol? :str string? :kws (spec/* keyword?)))

which matches the vectors

(spec/conform ::cat '[af "5"])
=> {:sym af, :str "5"}
(spec/conform ::cat '[af "5" :key])
=> {:sym af, :str "5", :kws [:key]}

but also the lists

(spec/conform ::cat '(af "5"))
=> {:sym af, :str "5"}
(spec/conform ::cat '(af "5" :key))
=> {:sym af, :str "5", :kws [:key]}

If we want to constrain this we could try using spec/tuple; but sadly it only matches fixed length vectors, i.e it requires at at the least an empty list to be the last part of the tuple:

(spec/def ::tuple (spec/tuple symbol? string? (spec/* keyword?)))
(spec/conform ::tuple '[af "5"])
=> :clojure.spec/invalid
(spec/exercise ::tuple)
=> ([[r "" ()] [r "" []]] [[kE "" (:M)] [kE "" [:M]]] ...)

We can also try adding on an additional condition to ::cat with spec/and:

(spec/def ::and-cat
  (spec/and vector? (spec/cat :sym symbol? :str string? :kws (spec/* keyword?))))

which matches fine

(spec/conform ::and-cat '[af "5"])
=> {:sym af, :str "5"}
(spec/conform ::and-cat '[af "5" :key])
=> {:sym af, :str "5", :kws [:key]}
(spec/conform ::and-cat '(af "5" :key))
=> :clojure.spec/invalid

but sadly fails in generating it's own data since the generator for spec/cat only produces lists which of course won't conform to the vector? predicate:

(spec/exercise ::and-cat)
=> Couldn't satisfy such-that predicate after 100 tries.

So to summarize: How does one write a spec which is both capable of accepting and generating vectors like [hi "there"] [my "dear" :friend]?

One could also rephrase the question as "Is there an alternative to spec/cat which generates vectors instead of lists?" or "Is it possible to pass a :kind argument to spec/cat?" or "Can I attach a generator to a spec which takes the output of the original generator and casts it to a vector?".

like image 947
Rovanion Avatar asked Oct 30 '22 09:10

Rovanion


1 Answers

Create the regex pattern independently from the spec:

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

(def pattern 
  (s/cat :sym symbol? :str string? :kws (s/* keyword?)))

(s/def ::solution
  (s/with-gen (s/and vector? pattern) 
              #(gen/fmap vec (spec/gen pattern))))

(s/valid? ::solution '(af "5" :key))  ;; false

(s/valid? ::solution ['af "5" :key])  ;; true

(gen/sample (s/gen ::solution) 4)
;; ([m ""] [. "" :Q] [- "" :?-/-9y :_7*/!] [O._7l/.?*+ "z" :**Q.tw.!_/+!gN :wGR/K :n/L])
like image 184
Alex Miller Avatar answered Jan 02 '23 20:01

Alex Miller