Fork me on GitHub
#clojure-spec
<
2019-01-13
>
borkdude11:01:46

I have a spec of map-entry and seqable-of-map-entry. Sometimes I get this exception while generating values:

(require '[clojure.spec.gen.alpha :as gen])
(require '[clojure.spec.alpha :as s])
(s/def ::map-entry
  (s/with-gen map-entry?
    (fn []
      (gen/fmap first
                (s/gen (s/and map? seq))))))
(s/def ::seqable-of-map-entry
  (s/coll-of ::map-entry :kind seqable?))

(gen/sample (s/gen ::seqable-of-map-entry))
Error printing return value (ClassCastException) at clojure.core/conj (core.clj:82).
java.lang.String cannot be cast to clojure.lang.IPersistentCollection

borkdude11:01:35

I wonder why it tries to call conj at all here

borkdude11:01:29

I can imagine it tries to build a seqable, so it starts with an empty string, and then it tries to conj map-entries to it:

(conj "" (first {:a 1}))

borkdude11:01:18

if this is a bug, I would be happy to file it in JIRA

borkdude11:01:40

for now I can use this workaround:

(s/def ::seqable-of-map-entry
  (s/with-gen (s/coll-of ::map-entry :kind seqable?)
    (fn []
      (s/gen (s/coll-of ::map-entry :kind list?)))))

borkdude12:01:06

I could also add :into [] but that would exclude lazy seqs

Alex Miller (Clojure team)13:01:27

coll-of always gens a collection, never a lazy seq

borkdude13:01:36

Yes, I meant, if I add the into, it would generate correctly but it would realize lazy seqs

borkdude12:01:32

maybe something like this? (doesn’t work yet)

(s/def ::seqable-of-map-entry
  (s/coll-of ::map-entry :kind (s/with-gen seqable?
                                 #(s/gen vector?))))

borkdude12:01:29

so kind must be a predicate and cannot be a spec, but it must also generate. in other words, you can only use pre-defined predicates?

borkdude12:01:18

this seems to work:

(defn seqable-of
  "Prevents generating strings and therefore Exceptions during generation"
  [elt-spec]
  (s/with-gen (s/coll-of elt-spec :kind seqable?)
    #(s/gen (s/coll-of elt-spec :kind vector?))))

Alex Miller (Clojure team)13:01:29

:kind seqable? does not make sense

Alex Miller (Clojure team)13:01:54

coll-of always gens a collection

Alex Miller (Clojure team)13:01:37

It’s a collection spec

borkdude14:01:40

What’s the recommended way of spec’ing a seqable of something then?

borkdude14:01:32

Maybe seqable and only checking the first value?

borkdude14:01:20

Maybe just coll-of would work, since

(coll? (seq {:a 1 :b -1 :c 1 :d -1}))
(coll? (filter (comp pos? val) {:a 1 :b -1 :c 1 :d -1}))
are both true

borkdude15:01:38

nope, it really should be a seqable, since (java.util.HashMap. {:a 1}) is also supposed to work.

borkdude15:01:28

every might be the one I should use then

borkdude15:01:07

that’s it. every also only checks a maximum number of elts, so a lazy infinite seq would still be supported. thanks. 🦆

borkdude15:01:15

isn’t this a bit inconsistent, since nil puns as an empty sequence?

user=> (s/conform (s/every string?) '())
()
user=> (s/conform (s/every string?) nil)
:clojure.spec.alpha/invalid
user=> (s/conform (s/every string? :min-count 0) nil)
:clojure.spec.alpha/invalid
user=> (count nil)
0

borkdude15:01:45

(s/conform (s/every string? :kind seqable?) nil) works though, but then I’m back into the same problem where I started:

user=> (gen/sample (s/gen (s/every string? :kind seqable?)))
Error printing return value (ClassCastException) at clojure.core/conj (core.clj:82).
java.lang.String cannot be cast to clojure.lang.IPersistentCollection

borkdude15:01:53

so maybe (s/nilable (s/every ::map-entry)) is best then

Alex Miller (Clojure team)16:01:02

:kind seqable? is just not right here. every (like coll-of) is a spec for collections (not seqables). you are using a broader predicate for :kind than the spec is intended for.

borkdude16:01:36

why is (s/nilable (s/every ::map-entry)) not the right fit for seqables? the implementation uses seq on the input and then checks every element up to a limit. so if this is not it, what’s the alternative?

borkdude16:01:37

if you don’t specify the kind to every, what’s the default?

Alex Miller (Clojure team)16:01:54

I’m saying seqable? does not make sense with coll-of/every because some seqables are not collections

Alex Miller (Clojure team)16:01:09

The default is vector iirc

borkdude18:01:00

@alexmiller would this be OK?

(defn seqable-of [spec]
  (s/with-gen (s/and seqable?
                     (s/or :empty empty?
                           :seq (s/and (s/conformer seq)
                                       (s/every spec))))
    #(s/gen (s/nilable (s/every spec :kind coll?)))))