Fork me on GitHub
#clojure-spec
<
2020-01-17
>
yuhan14:01:47

Is there any way of relating a neighboring spec's value into the :count option of s/every ?

yuhan14:01:48

eg. I have a map spec with keys :width and :things

yuhan14:01:21

and I want to ensure that things has width number of elements

Alex Miller (Clojure team)14:01:47

you need to s/and with a predicate that can do that at the containing level

yuhan14:01:52

okay, I have something like this

(s/def :ctx/region
  (s/and (s/keys :req [:ctx/width
                       :ctx/slots])
    #(= (count (:ctx/slots %))
        (:ctx/width %))))

yuhan14:01:45

so I'll have to write a custom generator if I want to gen examples of the spec?

Alex Miller (Clojure team)15:01:40

this is true of any data with internal constraints like this. the general strategy is to use gen the width first, then use a combination of fmap and bind to generate the slots and assemble into the map

Alex Miller (Clojure team)15:01:13

you might also ask what the width is buying you in this data structure when it is the same as the count of the slots

Alex Miller (Clojure team)15:01:52

(and as an aside, I find that pushing on data that is hard to spec often improves the shape of the data and the code that uses it)

yuhan15:01:47

My idea was to have a degree of "memoization" or redundancy baked into the data structure - in this case slots may be a lazy sequence that is generated on demand, so I don't want to keep calling count on it unnecessarily

yuhan15:01:39

of course this introduces the risk of width getting out of sync with the data, which is why I'm using spec to assert the constraints during dev time

yuhan15:01:28

there are a few other "derived" keys similar to this that I'm storing in the same data structure, just wondering if this isn't considered a code smell?

Alex Miller (Clojure team)15:01:55

it really depends

đź‘Ś 4
yuhan15:01:10

as with so many things 🙂

yuhan15:01:12

Here's my best attempt at that generator:

(s/def :ctx/width (s/int-in 1 11))
(s/def :ctx/slot string?)
(s/def :ctx/slots (s/coll-of :ctx/slot))
(s/def  :ctx/region
  (s/and (s/keys :req [:ctx/width
                       :ctx/slots]
           :gen #(let [w (gen/generate (s/gen :ctx/width))]
                   (gen/hash-map
                     :ctx/width (s/gen #{w})
                     :ctx/slots (s/gen (s/coll-of :ctx/slot
                                         :count w)))))
    #(= (count (:ctx/slots %))
        (:ctx/width %))))

Alex Miller (Clojure team)15:01:41

you don't want to use gen/generate in there - that will foil the test.check shrinking mechanisms

Alex Miller (Clojure team)15:01:34

instead, use gen/bind with (s/gen :ctx/width) and the gen/hashmap you have

Alex Miller (Clojure team)15:01:52

gen/bind lets you make a generator on a generator

yuhan18:01:49

got it, thanks so much for the help!

#(gen/bind (s/gen :ctx/width)
   (fn [w]
     (gen/hash-map
       :ctx/width (gen/return w)
       :ctx/slots (s/gen (s/coll-of :ctx/slot :count w)))))

đź’Ż 4
zane22:01:44

Let’s say I have a Datomic query and I want to find all the variables defined in that query. Would spec be a good tool to bring to bear on that problem?

seancorfield22:01:52

@zane I'm not saying it couldn't be done but I certainly wouldn't expect to use Spec for such a problem.

âś… 4
seancorfield22:01:43

Even if you wrote a complete Spec of Datomic queries, if you s/conform it so it "identifies" the ? variables, you'd still have to walk the resulting data structure to extract them all -- you might just as well walk the original Datomic query.

zane23:01:24

Yeah, I had imagined doing something clever with s/conform and clojure.walk.

zane23:01:29

But I see what you mean.

Alex Miller (Clojure team)22:01:32

I believe there are specs of Datomic's query syntax out there btw, don't have any links handy (but I agree that I probably wouldn't pick that as the first approach)

zane23:01:08

Good to know!

zane23:01:44

Is it fair to say that conform is not really suited to these kinds of problems in the general case?

seancorfield23:01:23

conform is not designed for parsing or general transformation, if that's what you're asking?

seancorfield23:01:06

conform is intended to "tag" the resulting data with "how" the data conformed to the Spec, so downstream code can behave accordingly.

zane23:01:07

Isn’t that kind of tagging exactly what I’m after in this case? I’m not sure I’m understanding the details here.

seancorfield23:01:12

You'd still have to walk the result to find the tagged values tho

zane00:01:29

I guess I’m still struggling with which kinds of “parsing” are appropriate to do with spec and which aren’t.

zane00:01:03

Differentiating between different shapes of data seems like something you could achieve with it via s/or and s/conform.

zane00:01:19

But you could also do that kind of thing with match, or with plain old Clojure.

seancorfield23:01:10

That's not to say that some people don't (ab)use Spec to do some amount of parsing and transformation... cough ...but that's not what it was designed for: coercion is somewhat of a "side-effect" of conforming, you might say 🙂

zane23:01:37

That makes sense.

zane23:01:56

I find myself wanting something like Instaparse, but for EDN rather than strings, often.