Fork me on GitHub
#clojure-spec
<
2018-03-13
>
rahulr9203:03:03

Hi, can someone help me understand the difference between spec/and and spec/merge? The example of merge given in the Clojure Spec guide https://clojure.org/guides/spec (using :animal/common and :animal/dog) seems to work even if we use and instead of merge.

seancorfield04:03:02

@rahulr92 s/merge will merge two s/keys specs (and, if I recall correctly, does not flow the conformed value); s/and is for combining a general spec with a general predicate (or spec) -- and it does flow the conformed value.

seancorfield04:03:15

Also, if you look at the docstrings, the differences should be reasonably clear https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/merge

seancorfield04:03:22

In the merge docs, you'll see another difference: it generates maps that conform to all the merged specs. With and, the first spec is used as the generator, and then the other specs are used as predicates to filter the generated values (which may not satisfy those predicates and so overall generation of values can fail).

rahulr9204:03:50

@seancorfield Thanks for the help. I understood the difference in generated value. However I still do not fully understand the difference while using these for validation. Could you please elaborated on what you mean by the ‘flow’ of conformed value? > ’Unlike ‘and’, merge can generate maps satisfying the union of the predicates. Doesn’t and also perform union i.e. it passes validation only if all the specs in it pass?

seancorfield04:03:36

@rahulr92 Here's an example showing a difference

boot.user=> (require '[clojure.spec.alpha :as s])
nil
boot.user=> (s/def ::a (s/or :i int? :s string?))
:boot.user/a
boot.user=> (s/def ::b (s/or :b boolean? :s string?))
:boot.user/b
boot.user=> (s/def ::merged (s/merge (s/keys :req [::a]) (s/keys :req [::b])))
:boot.user/merged
boot.user=> (s/def ::anded (s/and (s/keys :req [::a]) (s/keys :req [::b])))
:boot.user/anded
boot.user=> (s/conform ::merged {::a 1 ::b "x"})
#:boot.user{:a [:i 1], :b [:s "x"]}
boot.user=> (s/conform ::anded {::a 1 ::b "x"})
:clojure.spec.alpha/invalid
boot.user=>

seancorfield04:03:33

If you look at the result of conforming that map against (s/keys :req [::a]) you'll see why it fails with s/and but works with s/merge:

boot.user=> (s/conform (s/keys :req [::a]) {::a 1 ::b "x"})
#:boot.user{:a [:i 1], :b [:s "x"]}
boot.user=>

seancorfield04:03:21

and finally, regarding generation:

boot.user=> (s/exercise ::merged)
([#:boot.user{:a 0, :b false} #:boot.user{:a [:i 0], :b [:b false]}] [#:boot.user{:a "", :b "N"} #:boot.user{:a [:s ""], :b [:s "N"]}] [#:boot.user{:a "u", :b "N"} #:boot.user{:a [:s "u"], :b [:s "N"]}] [#:boot.user{:a "", :b false} #:boot.user{:a [:s ""], :b [:b false]}] [#:boot.user{:a "8", :b true} #:boot.user{:a [:s "8"], :b [:b true]}] [#:boot.user{:a "u", :b "k"} #:boot.user{:a [:s "u"], :b [:s "k"]}] [#:boot.user{:a 2, :b "IzO"} #:boot.user{:a [:i 2], :b [:s "IzO"]}] [#:boot.user{:a 1, :b "8s0"} #:boot.user{:a [:i 1], :b [:s "8s0"]}] [#:boot.user{:a 0, :b "QsU1J"} #:boot.user{:a [:i 0], :b [:s "QsU1J"]}] [#:boot.user{:a 11, :b true} #:boot.user{:a [:i 11], :b [:b true]}])
boot.user=> (s/exercise ::anded)

clojure.lang.ExceptionInfo: Couldn't satisfy such-that predicate after 100 tries.

seancorfield04:03:41

(which is no surprise because the ::anded spec won't accept anything)

rahulr9205:03:28

@seancorfield Great explanation. This really helps. Thanks a lot!

tbaldridge16:03:35

I'm not sure if anyone else has had this issue before, but I have a really strange spec problem that has shown up twice: I have a spec that defines a or:

tbaldridge16:03:03

(s/def ::val (s/or :int integer? :symbol :simple-symbol?))

tbaldridge16:03:37

And then I put that inside something else: (s/def ::coll-of-stuff (s/coll-of ::val))

tbaldridge16:03:07

But when I try to validate some data, explain fails and tells me: (s/explain ::coll-of-stuff '[x 1 2]) "value [:int 1] failed to conform to integer? or symbol?"

tbaldridge16:03:23

The problem is that something is trying to validate the conformed value? What's strange is I can't seem to reproduce this in a smaller situation. But so far it's always been with s/or and every time it tries to validate the conformed value

guy16:03:52

how are you doing the symbols out of interest?

tbaldridge16:03:07

doing the symbols?

guy16:03:15

(s/explain-data ::vals ['s 1])

guy16:03:18

like that?

tbaldridge16:03:32

that was part of the example here, I'm actually using keywords

guy16:03:33

The symbols in your data you are trying i mean sorry

guy16:03:01

Sorry i’m a little confused

tbaldridge16:03:46

So here's the actual error message:

In: [:ast/select-attrs 0] val: [:attr :bar] fails spec: :ast/select-attr at: [:ast/select :ast/select-attrs :attr] predicate: simple-keyword?
In: [:ast/select-attrs 0] val: [:attr :bar] fails spec: :ast/select-attr at: [:ast/select :ast/select-attrs :child] predicate: map?
nil

tbaldridge16:03:54

(namespace names changed for security reasons

tbaldridge17:03:02

And the relevant specs:

(s/def ::select-attr (s/or
                       :attr simple-keyword?
                       :child (s/keys :req [::from ::select-attrs ::logical-attr]
                                      :opt [::where])))
(s/def ::select-attrs (s/coll-of ::select-attr))

tbaldridge17:03:33

So what I don't understand is why it's saying [:attr :bar] is failing to conform to simple-keyword?

tbaldridge17:03:52

It's like or is trying to validate after it conforms the value or something.

jgdavey17:03:55

Wouldn’t [:attr :bar] be a (s/coll-of simple-keyword?), rather than a simple-keyword??

tbaldridge17:03:12

this is interesting, at the top level, there's a multi-spec. If I remove that it seems to work fine.

jgdavey17:03:16

Oh, I see, you’re saying that [:attr :bar] is what you’d get after conforming (s/conform ::select-attr :bar)

tbaldridge17:03:21

@jgdavey that's what's strange, the data I'm passing in looks like this [:foo :bar]. Hence the path in the error being [:ast/select-attrs 0] so it gets into the select-attrs path, and then the first item doesn't validate because [:attr :bar] is not a simple-keyword.

jgdavey17:03:21

Does using s/every instead of s/coll-of yield the same results?

tbaldridge17:03:21

wow, yep, I changed my ::select-op spec to this:

tbaldridge17:03:46

(s/def ::select-op (s/and
                     (fn [x]
                       (println "VALIDATE " x)
                       x)
                     (s/keys :req [::select-attrs ::from ::where])))

tbaldridge17:03:45

That's the top level spec. If I run it through multi-spec the values it tries to conform are [:attr :bar]. If I run it without multi-spec, the values are :bar

tbaldridge17:03:53

So multi-spec is doing something

tbaldridge17:03:23

let's see if I can get a minimal case now

Alex Miller (Clojure team)19:03:16

it is already marked thusly

cap10morgan20:03:56

ah, great. thanks!

tbaldridge17:03:18

wait...does:

(s/and ::foo
            ::bar)
pass the conformed value from ::foo to ::bar?

tbaldridge17:03:59

guess so: " Returns a spec that returns the conformed value. Successive conformed values propagate through rest of predicates."

tbaldridge17:03:03

boom that's it, @jgdavey I used s/and and that flows conformed values through the predicates (rather mis-leading name). Replaced it with s/merge and it works fine

jgdavey17:03:40

Interesting. That’s probably a subtly worth calling out in docs or something.

tbaldridge18:03:46

yeah, funny thing is that the docs are fairly clear, both s/and and s/merge mention it. I wonder if the behavior there changed somewhat recently

tbaldridge18:03:15

lol, nope, it's been like that since the start

mpenet18:03:29

I guess both could have options to control how/if the flow of conformed values is done

mpenet18:03:09

I tripped on the behavior of s/merge when I started with spec, so the other way around. I am not alone either

tbaldridge19:03:59

Yeah, I think part of it is that names are a tad overloaded in English. Merge makes sense to me since I'd expect (s/merge ...) to and things together. But it seems like a better name for s/and would be s/flow or something like that.

tbaldridge19:03:10

Maybe we should rename s/merge to s/smoosh 😛

Alex Miller (Clojure team)19:03:38

we considered having s/and (non-flowing) and s/and-> (flowing) at one point

tbaldridge19:03:54

s/& works the same way

Alex Miller (Clojure team)19:03:55

unfortunately the current s/and does the latter :(

tbaldridge19:03:10

but in the end, I didn't read the docs, so I'll slap my own hand this time

Alex Miller (Clojure team)19:03:11

yes, s/and and s/& flow. s/merge does not

Alex Miller (Clojure team)19:03:26

s/merge is conceptually pushing the value through all specs “in parallel”

Alex Miller (Clojure team)19:03:17

so flowing doesn’t make sense (except conforming returns the conformed result of the last spec which is hard to reconcile with that view)

tbaldridge19:03:11

So what's the common use-case for flowing specs? I think I've only ever used them with s/conformer, which I tend to avoid these days

Alex Miller (Clojure team)19:03:23

if you flow through a regex you’ll get structure which you can then easily apply predicates to

bronsa20:03:03

naming vars & is not the best idea

Alex Miller (Clojure team)20:03:28

it was originally called andre :)

misha20:03:39

flowing is handy when you have s/keys spec checking map's keys and vals independently, and then you apply predicate checking inter-kv-dependencies.

misha20:03:46

although, thinking about it a moment longer – this use case is sort of orthogonal to flowing :D

tbaldridge20:03:12

'Is it pronounced "an-dray" or "and-re"?'

tbaldridge20:03:51

Would be the new "a-sosh" or "a-sock". 😄