Fork me on GitHub
#clojure-spec
<
2017-06-28
>
lwhorton16:06:30

whats the proper way to define a spec referring to another namespace?

(s/def ::foo :bar.alice/bob)
(s/def ::my-spec (s/keys :req-un [::foo]))

lwhorton16:06:09

for some reason i get complaints about unable to resolve spec :bar.alice/bob using the above method

lwhorton16:06:56

might i be required to be explicit in :require [bar.alice :as bar.alice] so load-order is taken care of properly?

seancorfield16:06:58

The ns containing that spec (`:bar.alice/bob`) must be loaded before you can use it in another spec. Otherwise the s/def will not have been executed.

stathissideris17:06:27

is there a way to override the default generator of s/keys but only for a single key? (without renaming the key)

bbrinck17:06:47

We have some JSON data coming in over the wire that uses strings as keys e.g. {"city" "Denver" "state" "CO"}. We’ve been converting all strings keys to keywords in order to spec them with s/keys, but of course, doing the mapping takes a call to clojure.walk/keywordize-keys and then, if there is a problem that we want to report to the dev, we might need to convert back to strings to make a sensible error about the data. In this case, do people do the conversion like this? It would seem like being about to have string keys would be useful e.g. (s/keys :req-str [:location/city :location/state]) where :req-str is like :req and :req-str-un would be like :req-un, but for string keys

Alex Miller (Clojure team)19:06:32

bbrinck: would be reasonable to file a jira enhancement for htis

grzm17:06:55

I had some slow checks that I was trying to performance tune. One approach I wanted to try was swapping out the fdef spec during the stest/check run, similar to how you can swap out generators using the :gen option. I tried instrumenting the function and using the :spec option prior to the stest/check run, but that didn't have an effect. Does this seem like a reasonable thing to do? Is there a way to do it?

Alex Miller (Clojure team)19:06:40

grzm: check takes a generator override map - did you try that?

grzm19:06:31

It's not clear to me how providing a generator would replace the function spec: I'm not trying to change the values that are supplied to the function: I'm trying to change the spec that's applied to the function as a whole for the scope of the check. Or am I misunderstanding?

grzm19:06:56

What would the generator be generating in this case?

Alex Miller (Clojure team)19:06:36

oh, then instrument with a replacement spec should be what you want

Alex Miller (Clojure team)19:06:21

however, calling instrument correctly is sometimes hard

Alex Miller (Clojure team)19:06:41

in particular, anything that is being changed (including the thing whose spec is changing) needs to be in the list of instrumented vars. You should see that var in the return value from instrument as well - if you don’t, it wasn’t changed.

grzm19:06:36

I'm working up a short example that describes what I'm trying.

Alex Miller (Clojure team)19:06:04

here’s a spec replacement example:

(defn a [x] (if (zero? x) "a" (inc x))) ;; special behavior on 0
(defn b [y] (if (zero? y) 0 (+ (a y) 10))) ;; but b guards this
;; so use simpler spec when testing b
(stest/instrument `a 
  {:spec {`a (s/fspec :args (s/cat :a int?) :ret int?)}})
(stest/check `b)

grzm19:06:01

(defn a [x])

(s/fdef a
        :args (s/cat :x int?)
        :fn (fn [_] true))

(s/fdef b
        :args (s/cat :x int?)
        :fn (fn [_] false))

;; should pass
(stest/check `a)

(stest/instrument `a {:spec {`a `b}})
;; should fail
(stest/check `a)

grzm19:06:08

What I've been trying is more direct. In your example, you're checking b which calls a. I'd like to check a directly.

Alex Miller (Clojure team)19:06:19

I’m not sure the val of the :spec map will actually resolve that via the registry - did you try passing the actual spec there?

Alex Miller (Clojure team)19:06:57

(stest/instrument `a {:spec {`a (s/get-spec `b)}})

Alex Miller (Clojure team)19:06:53

doesn’t seem like that works either

grzm19:06:57

I don't see it working, either. You're a faster typer than I am 🙂

grzm19:06:13

Taking a step back, does this seem like a reasonable thing to do?

grzm19:06:07

(stest/instrument `a {:spec {`a (s/fspec :args (s/cat :x int?) :fn (fn [_] false))}})

grzm19:06:14

That doesn't work either, btw (for completeness)

grzm19:06:48

I'm considering taking your spec course at the Conj. How far into the weeds are you going to get? Do you think you'll have a syllabus available?

Alex Miller (Clojure team)20:06:58

the example I gave you above is from the spec course

grzm20:06:51

Gotcha. 🙂

grzm20:06:50

Would you like me to open a ticket for this?

Alex Miller (Clojure team)20:06:30

sorry, I was looking at the code. I understand why it doesn’t work, still thinking about what that means though.

Alex Miller (Clojure team)20:06:57

the spec override is used when building the wrapped var in instrument

Alex Miller (Clojure team)20:06:35

but check still uses the version in the spec for checking ret and fn, not the overridden spec

Alex Miller (Clojure team)20:06:03

and indeed check can’t even see that - it’s just part of the check-fn on the instrumented var

Alex Miller (Clojure team)20:06:37

seems like it would be totally reasonable to want to do something like this in the context of check though

Alex Miller (Clojure team)20:06:06

instrumented vars will only catch invalid invocations, not invalid results.

Alex Miller (Clojure team)20:06:21

so I think a ticket to add the ability for check to take spec overrides etc would be reasonable. we have talked about making things like generator overrides, spec overrides etc a consistent api used across exercise, exercise-fn, instrument, check, etc which would be in this area as well

grzm20:06:51

Okay. I'll open one. (No need to apologize, by the way. I suspected as much, and even if you were doing something else, I'm grateful for the time and attention you're providing.)

grzm20:06:23

I was similarly surprised about generator overrides with instrument and check. I expected generator overrides to apply globally. From what I can tell, they only apply to the checked function, not the functions called by the checked function.

Alex Miller (Clojure team)20:06:47

it’s a little more subtle than that

Alex Miller (Clojure team)20:06:01

they do apply globally, but there are a couple cases where they don’t get picked up

Alex Miller (Clojure team)20:06:05

there are some tickets on this

grzm20:06:10

Happen to have pointers to those tickets handy to see if they cover my use case?

Alex Miller (Clojure team)20:06:01

https://dev.clojure.org/jira/browse/CLJ-2095 is another, although I’m not sure that this is something we actually can or will change

grzm20:06:36

Are you guys happy with Jira? Is it worthwhile for someone to make a Clojure syntax highlighter plugin?

grzm23:06:38

Here's a gist showing the instrument generator override behavior I mentioned above. https://gist.github.com/grzm/0ada176caee5bcdab39d820577ed3823 I'll bring this up in #clojure-spec as well and continue discussion there unless you bring in back here. Thanks again for your help today.

lwhorton17:06:55

@bbrinck what’s your concern, performance?

bbrinck17:06:47

Performance and the fact the validated data doesn't match the original, so error messages either need to be converted back or are somewhat confusing.

lwhorton17:06:26

as always with performance i would test and verify that it’s going to be an issue before worrying about doing 2x conversions

grzm18:06:31

@bbrinck with respect to the validated data doesn't match the original, you're already doing a conversion (I suspect) from JSON objects to Clojure maps. The similarity between the two (including syntax) obscures this a bit.

bbrinck18:06:57

@lwhorton. Fair point about perf. My greater concern is complexity of implementation when converting to validate and then back to report for what seems like a common case of string keys (when working with JSON).

bbrinck18:06:24

Frankly it's not a huge cost. Just wondering if this workaround is how others are tackling this case.

lwhorton18:06:46

maybe im looking at it wrong, but i dont see complexity around (keywordize-keys json) and (string-keys edn)

lwhorton18:06:51

if you convert to edn, run a spec/validate and it fails, then convert it back to json and make a POST… that all seems fairly straightforward?

bbrinck18:06:32

I'd ideally like to give error messages that are relevant to the original data. Imagine an API that accepts JSON. If I want to give an errr message about non conformance, I want this to be about strings not keywords.

bbrinck18:06:16

I suppose I can explain-data and then walk each problem and convert back to strings. The n format the data?

lwhorton18:06:14

or hold a reference to the original json, right?

bbrinck18:06:52

But that's just the whole thing right? That's not the specific section that has the problem

lwhorton18:06:57

i suppose if you wanted to get into the exact location where a conform failed, yes you might have to do explain-data

bbrinck18:06:24

You'd need to walk each problem and reconvert, then reformat. But you are correct, it's not a huge amount of work

lwhorton18:06:57

you could also write your spec to be some form of s/cat :value string? ... etc.

bbrinck18:06:06

And I reformat I just mean reimplement 'explain-str'

lwhorton18:06:08

but that might be way more complex than what is already offered by (s/keys)

lwhorton18:06:43

(essentially spec out the JSON implementation)

lwhorton18:06:22

i would also look around for some libs that might already exist for this sort of thing

bbrinck18:06:38

Right. Thanks for the ideas! Good to know I'm not missing an obviously simpler way :)

lwhorton18:06:34

with plumatic.schema there was coersion, but i’m not sure if spec went down that path as well

bbrinck18:06:59

AIUI, spec is avoiding coercion but I could be mistaken. I might just end up implanting a version of 's/keys' that accepts strings (and produces a generator that uses string keys). Could be useful for JSON validation

bbrinck18:06:31

Note I agree the specs themselves should be keywords. I'm just thinking that they could be included in maps as strings.

lwhorton18:06:39

I’ve got a question for you 🙂 … have you ever used branching in a spec to define a spec? Say you have a key :foo and :bar, but :bar can pull from set A or B of valid values. Is there a way to make :bar read :foo and decide which is a valid spec?

grzm18:06:15

@lwhorton Does multi-spec do what you want? Or are you getting at something different?

lwhorton18:06:25

hah, that looks exactly like what I’m talking about

dealy21:06:28

spec newbie here, please excuse my ignorance. I have a data structure which I've defined a spec for. I've just needed to add a channel as one of the items. How do I update my spec for this type of a field?

seancorfield21:06:59

Pretty sure there’s no way to spec a channel (same as there’s no way to spec atom/delay/etc) so you can just use any? (or not provide an actual spec for that key).

seancorfield21:06:42

Spec’ing something to be a channel wouldn’t be very useful: you couldn’t generate values for it (since it has type channel).

dealy21:06:30

oh, ok, there's a lot of spec I'm still pretty confused about. So, spec'ing data structures is primarily for building up things from basic types (ints doubles bools and functions) as opposed to interfaces and complex objects?

seancorfield21:06:19

Hmm, not really…

seancorfield21:06:23

You can spec domain-level entities — so you can give things meaning — but you need to think about what you’re trying to achieve with spec. Why are you spec’ing a particular structure? What are you going to use the spec for?

seancorfield21:06:57

There’s not much point in spec’ing everything and I think it’s a common misunderstanding to try to treat spec like a type system and “spec everything”.

seancorfield21:06:25

You also need to consider whether you want specs to be “generatable” (you probably mostly do) so you need to avoid specs with types that can’t be generated (since they’re not very useful, in general).

dealy21:06:11

yea, that makes sense. I haven't gotten into that part yet. Still only using it as a way of just making sure my datastructure remains in the format I expect/need.

seancorfield21:06:23

Are you using conform or valid? or some other way of checking against the spec?

dealy21:06:38

this is all part of my first GUI app using re-frame so its validating the main app-db as the UI changes

seancorfield21:06:56

Since spec checks are all runtime, if you really need to “assert” your data structure has a specific key, you can always spec it as any? and if you use something as a channel and it isn’t, it’s going to blow up at runtime anyway.

dealy21:06:09

its a lot to learn all at once

seancorfield21:06:13

I would be a bit suspicious of data including a channel tho’…

dealy22:06:00

normally yes, but it is an artifact of a timer that needs to be cleaned up later

seancorfield22:06:01

after all, that’s not data you can serialize and pass around so it’s not really data

seancorfield22:06:19

(I may be taking a bit of a purist view here — not sure how others feel?)

lwhorton22:06:31

can someone point out my (probable) misuse of multi-spec? what i’m trying to go for, in plain english, is to specify a map where some key’s spec depends on the value of another key. If key a is one of “cat” or “dog”, key b should be spec’d differently in either case.

(defmulti b-depends-on-a :a)
(defmethod b-depends-on-a "cat" [_] (s/or :cat-only-thing ::cat-thing
                                          :cat-or-dog-thing ::shared-things))
(defmethod b-depends-on-a "dog" [_] (s/or :dog-only-thing ::dog-thing
                                          :cat-or-dog-thing ::shared-things))
(s/def ::a #{"cat" "dog"})
(s/def ::b (s/multi-spec b-depends-on-a :a))
(s/def ::foo (s/keys :req-un [::a
                              ::b]))

lwhorton22:06:35

I would expect to be able to run something like (s/conform :: foo {:a "cat" :b ...}) or (s/conform :: foo {:a "dog" :b ...}) and have the response [:cat-only-thing …] or [:dog-only-thing …] or [:cat-or-dog-thing ...] depending on the value of b.

lwhorton22:06:39

Is this totally off?

seancorfield22:06:02

I think your multi-spec needs to be a hash map that has a key :a

seancorfield22:06:49

So ::b would be spec a hash map — do ::cat-thing etc spec hash maps?

lwhorton22:06:18

i’ve been banging my head on that page for quite a while now .. i dont’ really understand the ‘tag’ argument to be honest

seancorfield22:06:20

This https://clojure.org/guides/spec#_multi_spec also shows the multi-specs as being hash maps.

lwhorton22:06:08

the call to s/multi-spec event-type in that example is calling on a defmulti event-type, unless I’m misunderstanding

seancorfield22:06:53

The :event/type key is common to all the (hash maps) that are valid for that set of specs.

seancorfield22:06:02

In your example, you should have a hash map that has a key :a that is the “tag” and it can have the value "cat" or "dog" and the defmethods should returns specs for the cat, or dog, hash map that includes the tag :a.

lwhorton22:06:47

oh you mean the object on which i’m invoking the spec? i.e. s/conform ::foo {:a "cat" (or "dog") :b something-else}?

seancorfield22:06:37

Right… so your defmethods should be (s/keys :req-un [::a :cat/b]) and (s/keys :req-un [::a :dog/b])

seancorfield22:06:23

then the multi-spec selects which spec to use based on the tag :a

lwhorton22:06:29

ah so i guess I cannot get away with what I was originally trying to do

seancorfield22:06:58

Depends what ::cat-thing, ::dog-thing, and ::shared-things are…

lwhorton22:06:18

which is something like “if the multimethod matches on ‘cat’, return a spec which is an (s/or …)

lwhorton22:06:46

so you are saying ::cat-thing, ::dog-thing, ::shared-things would have to be maps containing the key :a, correct?

seancorfield22:06:43

What are you trying to do?

seancorfield22:06:12

If you explain how you want :b to vary, I can probably show you…

lwhorton22:06:17

i have a piece of data that looks like this {:status "" :operation "" :state ""}

lwhorton22:06:41

actually let me thin that down even more, i have a map {:status "" :operation ""}

lwhorton22:06:16

operation can belong to any member of the set #{:idle :downloading :patching}

lwhorton22:06:41

but depending on the value of :operation, the spec for status should be “limited”. that is, given the idle operation, the available statuses should only be one of #{:a :b :c}, and given the downloading operation, statuses should only be one of #{:c :d :e} (note the overlap)

lwhorton22:06:58

i was trying to use spec to enforce this pattern-matching behavior

lwhorton22:06:57

the (s/or ..) comes into play because there is some overlap between :downloading / :patching / :idle, but only 1-2 states

seancorfield22:06:08

Sure… so you need to spec the different types of status values, e.g., (s/def :downloading/status #{:c :d :e}) and (s/def :idle/status #{:a :b :c})

seancorfield22:06:01

and then your defmethod would return (s/keys :req-un [::operation :downloading/status]) for the downloading branch and (s/keys :req-un [::operation :idle/status]) for the idle branch

lwhorton22:06:05

oof, so all this pain and confusion because i was trying to do an s/or (to union) where I should have just defined the full sets outright, regardless of overlaps?

lwhorton22:06:49

ill give that a whirl and get back to you.. regardless of the outcome thanks for your help

seancorfield22:06:41

(s/def ::operation #{:idle :downloading :patching})

(s/def :idle/status #{:a :b :c})
(s/def :downloading/status #{:c :d :e})
(s/def :patching/status #{:a :c :f})

(defmulti operation-spec :operation)
(defmethod operation-spec :idle
  [_] (s/keys :req-un [::operation :idle/status]))
(defmethod operation-spec :downloading
  [_] (s/keys :req-un [::operation :downloading/status]))
(defmethod operation-spec :patching
  [_] (s/keys :req-un [::operation :patching/status]))

(s/def ::machine (s/multi-spec operation-spec :operation))

seancorfield22:06:59

(s/conform ::machine {:operation :idle :status :a}) succeeds

seancorfield22:06:04

(s/conform ::machine {:operation :idle :status :d}) fails

seancorfield22:06:28

(s/conform ::machine {:operation :downloading :status :a}) succeeds

lwhorton22:06:41

indeed it does, and a gen/sample gives me 10 good values !

lwhorton22:06:22

well it’s good to know now that defmethod has to return “the original match” + the spec, not some hob-goblin pseudo spec

lwhorton22:06:26

many thanks for your guidance

seancorfield22:06:53

Thanks for the question — I hadn’t played with multi-spec before and this gave me a good opportunity to try it out.

seancorfield22:06:58

The different branches could also have different keys etc.

seancorfield22:06:28

And, indeed, doesn’t have to be a hash map but then the tag function would need to be something other than a simple keyword.

lwhorton22:06:10

so does the tag arg on (multi-spec spec tag) have to be identical to the fn/kwd in defmulti name fn/kwd?

lwhorton22:06:09

not really sure why the tag is there.. unless its a way to generate fields under a different key from the one of the multi-match?

bbloom22:06:16

random thought: i kinda wish there was a way to alias keys, just as there is a way to alias attributes in datomic - would help with refactoring and “subtyping”

seancorfield22:06:11

@lwhorton I believe the tag is needed in multi-spec so that the generator can figure out what key to work with.

seancorfield22:06:29

(since defmulti could take a function etc)

lwhorton22:06:09

i see .. if you still have that ^above code in a register, what happens when you try to generate some samples?

seancorfield22:06:18

If defmulti was some complex selector function, the argument passed to multi-spec would have to kind of reverse that I think…

lwhorton22:06:45

do you end up with {:data-state "" :data-status {:data-state "" :data-status ""}?

seancorfield23:06:03

([{:operation :idle, :status :c} {:operation :idle, :status :c}] 
 [{:operation :patching, :status :c} 
  {:operation :patching, :status :c}] 
 [{:operation :downloading, :status :c} 
  {:operation :downloading, :status :c}] 
 [{:operation :idle, :status :b} {:operation :idle, :status :b}] 
 [{:operation :downloading, :status :e} 
  {:operation :downloading, :status :e}] 
 [{:operation :patching, :status :f} 
  {:operation :patching, :status :f}] 
 [{:operation :idle, :status :c} {:operation :idle, :status :c}] 
 [{:operation :idle, :status :b} {:operation :idle, :status :b}] 
 [{:operation :patching, :status :c} 
  {:operation :patching, :status :c}] 
 [{:operation :idle, :status :b} {:operation :idle, :status :b}]) 
from s/exercise

lwhorton23:06:34

interesting, i probably typo-d somewhere

lwhorton23:06:47

this spec gen stuff is so great

seancorfield23:06:35

Yup, I pretty much always try to s/exercise my specs as a sanity check, and also to see the sort of crazy stuff they can produce. We use regex fairly heavily so we rely on @gfredericks test.chuck which has a generator for regex string specs — awesome stuff!

spieden23:06:28

great clojure.test integration too =)