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 [email protected]
Note that posts from new members are moderated - please be patient with your
first post.
To unsubscribe from this group, send email to
[email protected]
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 [email protected].
For more options, visit https://groups.google.com/d/optout.