Building a Clojure spec generator for a nested level constrained data structure

I am using the Clojure spec to spec a simple data structure:

{:max 10
 :data [[3 8 1]
        [9 0 1]]}

      

The value :data

is a vector of vectors of equal size, integers ranging from zero to :max

inclusive. I expressed it with the BOM like this:

(s/def ::max pos-int?)
(s/def ::row (s/coll-of nat-int? :kind vector?, :min-count 1))
(s/def ::data (s/and (s/coll-of ::row :kind vector?, :min-count 1)
                     #(apply = (map count %))))

(s/def ::image (s/and (s/keys :req-un [::max ::data])
                      (fn [{:keys [max data]}]
                        (every? #(<= 0 % max) (flatten data)))))

      

Automatic generators work fine for the first three specs, but not for ::image

. (s/exercise ::image)

always fails after 100 attempts.

I tried to create my own generator for ::image

but couldn't. I don't see how I can express constraints crossing the layers of the nested structure (the key :max

constrains the values ​​in the vector somewhere else).

Is it possible to create a Clojure spec / test.check generator that generates ::image

s?

+3


source to share


1 answer


Sure! The key point here is to create the domain model. Here I think the model is maximum, col-size and row-size. This is enough to create a valid example.

So something like this:

(def image-gen
  (gen/bind
    (s/gen (s/tuple pos-int? (s/int-in 1 8) (s/int-in 1 8)))
    (fn [[max rows cols]]
      (gen/hash-map
        :max (s/gen #{max})
        :data (gen/fmap #(into [] (partition-all cols) %)
                (s/gen (s/coll-of (s/int-in 0 (inc max)) 
                                  :kind vector?
                                  :count (* rows cols))))))))

      

First, we create a set of [<max-value> <rows> <cols>]

. Then it gen/bind

returns a new generator that creates the maps in the desired shape. We embed gen/fmap

inward to construct a vector of all random data values, then reformat it into the appropriate embedded vector form.



Then you can combine this image with:

(s/def ::image
  (s/with-gen 
    (s/and (s/keys :req-un [::max ::data])
           (fn [{:keys [max data]}]
             (every? #(<= 0 % max) (flatten data))))
    (fn [] image-gen)))

      

It might be interesting to note that I limited the rows and columns to no more than 7, as the generator might otherwise try to generate very large random sample values. The need to link things like this is pretty common in custom generators.

With some extra effort, you can get more reusability from some of these generator specs and parts.

+4


source







All Articles