3) I agree that my argumentation was vague but the sentiment still holds true: I'd like to easily validate keyword args with the same level of confidence as I can validate positional args.
1,2) > Because all map keys are independent and don't rely on each other for ordering. > You can add additional checks to verify stricter things on top of open specs. You can't add openness to something that is already closed. If I understand you correctly, library currently is only concerned with doing simplest things and that's why `s/keys` is not very strict? But then I'm not sure if it's the simplest behaviour though since `s/keys` specs validate 2 things: keys AND (sometimes) values. And that "sometimes" part makes production code more error-prone. Is there any chance that in the future core.spec will have additional API to allow specifying other levels of map validation strictness? Possible options: * Keyset validation strictness: - open (current behavior, allows putting any additional keys into map) - closed (map must contain only the listed keys) * Values validation strictness: - never validate (i.e. any values will pass, not sure about the use cases though) - only if spec for key is registered (current behavior) - always validate values (should fail if there's no spec for value registered) Thanks. On Tuesday, October 3, 2017 at 6:43:56 PM UTC+3, Alex Miller wrote: > > > > On Tuesday, October 3, 2017 at 8:10:30 AM UTC-5, Yuri Govorushchenko wrote: >> >> 1) About `s/keys` silently ignoring missing value specs. My question was: >> "Is there any way to ensure that the keys I used in `s/keys` have the >> associated specs defined?." >> >> Specs can be defined or added later, so there is no valid way to do this. >> >> >> OK, so requiring that values are spec-ed can't be enforced at compilation >> time because this would make it impossible to define value specs after >> `s/keys`: >> >> ``` >> (s/def ::foo (s/keys :req [::bar])) >> ,,, >> (s/def ::bar ,,,) >> ``` >> >> This can explain why it's not built into the library. In such case I'm >> fine with using a custom macro instead of `s/keys` (see my gist in the >> previous post). >> >> But what about enforcing existence of value specs at runtime, during >> validation? `s/cat` does that, e.g.: >> >> ``` >> cljs.user=> (s/def ::foo (s/cat :bar ::baz)) >> :cljs.user/foo >> cljs.user=> (s/valid? ::foo [123]) >> Unable to resolve spec: :cljs.user/baz >> ``` >> >> > This is required because you are essentially parsing a sequential data > structure - one component could be a single value or many values, so you > must know it's definition. > > >> Why is it then not a default behaviour for `s/keys` as well? I.e.: >> > > Because all map keys are independent and don't rely on each other for > ordering. > > >> >> ``` >> ; current behavior >> cljs.user=> (s/def ::foo (s/keys :req [::x])) >> :cljs.user/foo >> cljs.user=> (s/valid? ::foo {::x 123}) >> true >> >> ; desired behavior >> cljs.user=> (s/def ::foo (s/keys :req [::x])) >> :cljs.user/foo >> cljs.user=> (s/valid? ::foo {::x 123}) >> Unable to resolve spec: :cljs.user/x >> ``` >> >> I don't think Spec-ulation Keynote addresses this behaviour. >> >> 2) There's no *built-in* way restrict the keyset of the map in >> `core.spec`. >> >> The reasoning for this seems to be based around the idea of backwards >> compatible evolving specs (see Spec-alution Keynote). But there are several >> good examples already covered in this thread which demonstrate that it >> would be very convenient to also have strict keyset validations available >> in `core.spec`. After all, the library is about specifying the structure of >> data and not only about specifying API contracts. >> > > You can add additional checks to verify stricter things on top of open > specs. You can't add openness to something that is already closed. > > >> >> 3) Thinking more about `s/keys` vs. `s/cat` *specifically* in the context >> of asserting API contracts (I'm stressing on the context here because >> `core.spec` is a general library and should take in account other use cases >> too). >> >> In this context it's even more apparent that `s/keys` should behave in >> the same way as `s/cat` because there's not much difference between >> positional arguments and keyword arguments. >> > > But there is - namely, the positional part. s/cat must know the spec for > every component to successfully conform the entire sequence. s/keys can > independently conform (or not conform) every key. > > >> I'll try to illustrate what I mean with an example. Let's say there's a >> function with positional arguments: >> >> ``` >> (defn foo-pos [x y z]) >> >> ; call example: >> (foo xxx yyy zzz) >> ``` >> >> I hope we can agree that it's more or less equivalent to this function >> with the keyword arguments where each keyword corresponds to the position >> number in `foo-pos`: >> > > Yeah but it's NOT equivalent. s/cat allows you to nest arbitrary > sequential structure in a component. The spec for this could be: > > (s/def ::last-args (s/cat :y any? :z any?)) > (s/fdef foo :args (s/cat :x any? :last ::last-args)) > > Without knowing the structure of ::last-args, s/cat can't validate the > input. > > Not going to respond to the rest of this post, as it uses this false > argument of equivalence. > > > >> >> ``` >> (defn foo-pos* [{x 1 y 2 z 3}]) >> >> ; call example: >> (foo-pos* {1 xxx 2 yyy 3 zzz}) >> ``` >> >> And making a step further to better naming: >> >> ``` >> (defn foo-kw [{:keys [::x ::y ::z]}]) >> >> ; call example: >> (foo-kw {::x xxx ::y yyy ::z zzz}) >> ``` >> >> So, the biggest difference is the syntax of function calls. Keyword >> arguments are usually more readable (esp. when there are several args) and >> easier to maintain (since they can be reordered at the call site and >> function definition safely). Let's now spec-ify the arguments using `s/cat` >> for positional args and `s/keys` for keyword args (as recommended in docs). >> These specs ensure that argument `x` is present and is of type `::x`, `y` >> is present and is of type `::y`, etc.: >> >> ``` >> (s/def ::foo-pos-args (s/cat :x ::x :y ::y :z ::z)) >> (s/def ::foo-kw-args (s/keys :req [::x ::y ::z])) >> ``` >> >> Now (because the functions are equivalent) I'd expect their specs to >> validate equivalent inputs in the similar way. But it's not the case if >> developer forgets to define the `::y` spec! >> >> ``` >> ; ::y spec is missing >> (s/def ::x int?) >> (s/def ::z int?) >> >> ; positional args >> (def pos-inputs [1 2 3]) >> (s/valid? ::foo-pos-args pos-inputs) ; => Unable to resolve spec: >> :cljs.user/y (good) >> >> ; keyword args >> (def kw-inputs {::x 1 ::y 2 ::z 3}) >> (s/valid? ::foo-kw-args kw-inputs) ; => true (ouch!) >> ``` >> >> TL/DR: (specifically in the context of function contracts) `core.spec` >> shouldn't treat APIs with positional arguments and APIs with keyword >> arguments differently and thus `s/keys` should check arg values at keys in >> the same way `s/cat` checks arg values at positions. >> >> -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.