Fork me on GitHub
#clojure-spec
<
2022-09-20
>
phronmophobic18:09:13

I'm trying to use spec to build a generator for valid destructure bindings. I'm starting with the specs in clojure.core.specs.alpha.

(s/def ::seq-binding-form
  (s/and vector?
         (s/cat :forms (s/* ::binding-form)
                :rest-forms (s/? (s/cat :ampersand #{'&} :form ::binding-form))
                :as-form (s/? (s/cat :as #{:as} :as-sym ::local-name)))))
Generating ::seq-binding-form values struggles because it's just generating random vectors that hopefully satisfy the s/cat. This seems to get fixed with (gen/fmap vec ...):
(s/def ::seq-binding-form
  (s/with-gen
    (s/and vector?
           (s/cat :forms (s/* ::binding-form)
                  :rest-forms (s/? (s/cat :ampersand #{'&} :form ::binding-form))
                  :as-form (s/? (s/cat :as #{:as} :as-sym ::local-name))))
    #(gen/fmap
      vec
      (s/gen
       (s/cat :forms (s/* ::binding-form)
              :rest-forms (s/? (s/cat :ampersand #{'&} :form ::binding-form))
              :as-form (s/? (s/cat :as #{:as} :as-sym ::local-name)))))))
Unfortunately, the version using gen/fmap now usually throws a stackoverflow exception. However, I noticed that if I ignore the vector requirement for ::seq-binding-form, then the stack overflow exception goes away.
(s/def ::seq-binding-form
  (s/cat :forms (s/* ::binding-form)
         :rest-forms (s/? (s/cat :ampersand #{'&} :form ::binding-form))
         :as-form (s/? (s/cat :as #{:as} :as-sym ::local-name))))
Is there a way to prevent and stackoverflow exceptions and have it generate a vector?

phronmophobic18:09:06

It looks like spec2 might have better support for this. I don't have any real reason to stick with spec1, so I'll see if I can get this working with spec2.

Ben Sless18:09:37

What if instead of fmap you use and conforming?

👀 1
phronmophobic18:09:07

I don't know about conforming, but I'll take a look

Ben Sless18:09:37

Maybe it's called conform, lemme check

phronmophobic18:09:23

giving it a try!

phronmophobic18:09:24

I don't think that solves this use case. It still doesn't generate ::seq-binding-form as a vector. It does convert it if I conform the value, but I think it's worth trying spec2

Ben Sless18:09:44

Gen doesn't emit conformed values?

phronmophobic18:09:30

It's totally possible I'm doing it wrong. My attempt was:

(s/def ::seq-binding-form
  (s/and
   (s/cat :forms (s/* ::binding-form)
          :rest-forms (s/? (s/cat :ampersand #{'&} :form ::binding-form))
          :as-form (s/? (s/cat :as #{:as} :as-sym ::local-name)))
   (s/conformer vec)))

Alex Miller (Clojure team)19:09:58

the specs need to be updated to have this work with spec 2

Alex Miller (Clojure team)19:09:48

to use catv instead of cat etc

👍 1
phronmophobic17:09:09

Just to follow up, test.check is magic. For the right problems, it finds tricky use cases that I would never think of.

> (let [[ & [ & [& [& [& [& hi]]]]]] [42]]
    hi)
(42)
> (let [[& {:as m}] [:a 42]]
    m)
{:a 42}

Alex Miller (Clojure team)17:09:27

generation isn't magic, but I'm pretty sure shrinking is

👆 1