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 ``` Why is it then not a default behaviour for `s/keys` as well? I.e.: ``` ; 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. 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. 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`: ``` (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. On Tuesday, October 3, 2017 at 6:01:06 AM UTC+3, Alex Miller wrote: > > > > On Monday, October 2, 2017 at 10:37:31 AM UTC-5, Yuri Govorushchenko wrote: >> >> Hi! >> >> I have some noobie questions for which I couldn't google the compelling >> answers. >> >> 1) 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. > > >> At compile time or at least at runtime. Maybe via an additional library? >> I could imagine a macro (smt. like `s/keys-strict` or `s/map-pairs`, as >> maps can also be viewed as sets of spec'ed pairs) which additionally checks >> that all keys have specs registered. I'm OK with sacrificing some >> flexibility (e.g. being able to define key specs after map specs, >> dynamically, etc.) in favour of more strictness. >> >> Motivation: I don't fully trust my map validation code when using >> `core.spec`. `s/keys` doesn't require that the key has the spec registered >> to validate its value. Although this may be flexible but in practice can >> lead to errors. Specifically, it's quite easy to forget to create a spec >> for a key, mistype it or forget to require the namespace in which key spec >> is defined (e.g. if the common key specs reside in a dedicated ns): >> >> ``` >> ; totally forgot to define a spec for ::foo >> (s/def ::bar (s/keys :req [::foo])) >> >> ; fooo vs. foo typo >> (s/def ::fooo string?) >> (s/def ::bar (s/keys :req [::foo])) >> >> ; :common/foo vs. ::common/foo typo >> (s/def ::bar (s/keys :req [:common/foo])) >> >> ; didn't require common.core ns (spec for :common.core/foo is not added >> to global registry) >> (s/def ::bar (s/keys :req [:common.core/foo])) >> ``` >> >> These subtle mistakes can lead to map validations passing silently (as >> long as keysets are correct). >> >> Related to this: there're feature requests for Cursive IDE which try to >> address typing and reading mistakes related to keywords, e.g. >> https://github.com/cursive-ide/cursive/issues/1846 and >> https://github.com/cursive-ide/cursive/issues/1864. >> >> After using Schema for a while it's difficult to appreciate the way >> `core.spec` defines it's own global registry which uses keywords instead of >> using spec instances and good old variables, especially since Cursive IDE >> has quite a nice support for variables already. But I think this is another >> topic which was already discussed, e.g. in >> https://groups.google.com/forum/#!topic/clojure/4jhSCZaFQFY ("Spec >> without global registry?"). >> >> 2) What is the motivation for library having a "loose" default behaviour >> of `s/keys` and no "strict" variant at all for spec-ing both keys and >> values at the same tome? I think in majority of cases I'd need to spec both >> keys and values of the map instead of only keys and would expect the >> library to have built-in API for this. Maybe for the future references it >> would be beneficial to add concrete code examples into motivation in the >> core.spec guide ( >> https://clojure.org/about/spec#_map_specs_should_be_of_keysets_only) >> which would better illustrate the described benefits of the current lib >> behaviour? >> > > > This is all about evolution of specs over time. If you make something > "strict" then you are saying "this is not allowed". You thus can never > extend the key set in the future without causing breakage. Whereas if you > say what a key set must have and validate what it does have, you can also > grow the spec in the future. Rich expanded at length on this idea and this > particular case in https://www.youtube.com/watch?v=oyLBGkS5ICk . > > Alex > -- 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.