clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
yuhan 2020-01-17T14:33:47.000700Z

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

yuhan 2020-01-17T14:34:48.001900Z

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

yuhan 2020-01-17T14:35:21.002700Z

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

alexmiller 2020-01-17T14:35:47.003200Z

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

yuhan 2020-01-17T14:36:52.003800Z

okay, I have something like this

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

alexmiller 2020-01-17T14:37:28.004400Z

yep

yuhan 2020-01-17T14:37:45.004800Z

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

alexmiller 2020-01-17T15:01:54.005100Z

yes

alexmiller 2020-01-17T15:03:40.006300Z

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

alexmiller 2020-01-17T15:04:13.006900Z

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

alexmiller 2020-01-17T15:04:52.007500Z

(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)

yuhan 2020-01-17T15:15:47.009900Z

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

yuhan 2020-01-17T15:16:39.010800Z

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

alexmiller 2020-01-17T15:18:51.011900Z

fair enough

yuhan 2020-01-17T15:19:28.012400Z

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?

alexmiller 2020-01-17T15:22:55.012800Z

it really depends

👌 1
yuhan 2020-01-17T15:31:10.013200Z

as with so many things 🙂

yuhan 2020-01-17T15:33:12.014600Z

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 %))))

alexmiller 2020-01-17T15:51:41.015100Z

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

alexmiller 2020-01-17T15:52:34.015800Z

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

alexmiller 2020-01-17T15:52:52.016100Z

gen/bind lets you make a generator on a generator

yuhan 2020-01-17T18:06:49.017400Z

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)))))

💯 1
zane 2020-01-17T22:32:44.018200Z

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?

seancorfield 2020-01-17T22:50:52.019100Z

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

✅ 1
seancorfield 2020-01-17T22:51:43.020400Z

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.

alexmiller 2020-01-17T22:57:32.021200Z

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)

zane 2020-01-17T23:15:08.021500Z

Good to know!

zane 2020-01-17T23:15:24.021700Z

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

zane 2020-01-17T23:15:29.021900Z

But I see what you mean.

zane 2020-01-17T23:20:44.022700Z

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

seancorfield 2020-01-17T23:22:23.023200Z

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

seancorfield 2020-01-17T23:23:06.023900Z

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

seancorfield 2020-01-17T23:24:10.025100Z

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 🙂

zane 2020-01-17T23:34:37.025400Z

That makes sense.

zane 2020-01-17T23:34:56.025900Z

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

zane 2020-01-17T23:36:07.026Z

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

seancorfield 2020-01-17T23:56:12.026700Z

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