Fork me on GitHub
#clojure-spec
<
2023-11-16
>
cfleming00:11:18

I’m trying to spec a map which I thought was homogeneous (i.e. key type matching a spec -> value type matching a spec), so I used map-of. Now it turns out that these maps can occasionally have other entries where the key is a constant keyword and then the value is something different to the other value type. I can’t figure out the best way to spec this - any advice?

Alex Miller (Clojure team)01:11:01

it's a bit cumbersome but you can spec the map as a collection of tuple (entry) types

cfleming01:11:28

I see, so I’d define two s/tuple types, and then a coll-of :kind map? using s/or over the two types?

Alex Miller (Clojure team)01:11:33

(s/def ::map-bindings
  (s/every (s/or :mb ::map-binding
                 :nsk ::ns-keys
                 :msb (s/tuple #{:as :or :keys :syms :strs} any?)) :into {}))

(s/def ::map-special-binding
  (s/keys :opt-un [::as ::or ::keys ::syms ::strs]))

(s/def ::map-binding-form (s/merge ::map-bindings ::map-special-binding))
is kind of the crux of that - an s/merge of special keys via s/every with an s/keys

cfleming01:11:26

Ok, thanks for the pointer, I think I’ll need some time to digest that but I can figure it out from there.

Alex Miller (Clojure team)01:11:19

that example is probably more complicated than what you need but that's the main idea

cfleming01:11:01

Can I s/merge an s/map-of with an s/keys?

Alex Miller (Clojure team)01:11:22

not successfully unless the map-of uses keyword keys

cfleming01:11:32

It doesn’t, unfortunately.

Alex Miller (Clojure team)01:11:14

it's merging the requirements of the two specs so any data has to satisfy both

Alex Miller (Clojure team)01:11:07

note that the tuple side (s/every of s/tuple) has an :msb branch that is effectively matching all of the known kws with any?

Alex Miller (Clojure team)01:11:38

and then the s/keys side is s/opt so it's just ignoring anything else

cfleming01:11:05

So when I do an s/merge, every entry has to match both sides of the merge? I’m not sure I understood that.

Alex Miller (Clojure team)01:11:55

you are merging the requirements of the spec

Alex Miller (Clojure team)01:11:06

so, it's kind of like s/and, but sub specs are effectively checked in parallel whereas s/and flows data through and checks sub specs serially

Alex Miller (Clojure team)01:11:12

this also has implications for s/conform but you may not care about that

cfleming01:11:46

Here’s my little test case:

(s/def ::normal-key string?)
  (s/def ::normal-val string?)
  (s/def ::constant (s/map-of ::normal-key ::normal-val))
  (s/def ::foo (s/coll-of string? :kind vector?))
  (s/def ::special-case (s/keys :req-un [::foo]))

  (s/conform ::constant {"foo" "bar"})
  (s/conform ::special-case {:foo ["test"]})

  (s/def ::mixed (s/every (s/or :constant (s/tuple ::normal-key ::normal-val)
                                :outlier (s/tuple #{:foo} ::foo))))

  (s/def ::end-result (s/merge ::mixed ::special-case))
  
  (s/conform ::end-result {"foo" "bar"})
  (s/conform ::end-result {:foo ["test"]}))

cfleming01:11:03

So I started with a map of string to string using ::constant. Then I realised I need ::special-case too. However my ::end-result can’t conform {"foo" "bar"}

cfleming01:11:57

(s/explain ::end-result {“foo” “bar”}) {“foo” “bar”} - failed: (contains? % :foo) spec: :cursive.extensions.specs/special-case

Alex Miller (Clojure team)01:11:29

req-un has to be opt-un there

Alex Miller (Clojure team)01:11:45

as string/string entries won't match that

cfleming01:11:22

Ah, of course - that works, thanks!