Fork me on GitHub
#clojure-spec
<
2018-08-24
>
guy10:08:36

:thinking_face:

misha10:08:42

beware of this:

(def some-keys [:a/key :a/nother-key])
(s/def :a/spec (s/keys :req (var-get some-keys)))
(s/valid? :a/spec {:a/key 1 :a/nother-key 2}
=> true

misha10:08:33

true is because (s/keys :req (var-get some-keys)) is equivalent to empty (s/keys)

misha10:08:18

(var-get some-keys) this does not work inside s/keys the way one would expect. (first of all, it should be (var-get #'some-keys))

guy11:08:19

@conan this is good to know!

conan12:08:09

ah yes, thanks for pointing this out!

👍 4
misha10:08:41

at least in clojure. mb in cljs it does

misha10:08:20

(s/exercise (s/keys :req (var-get some-keys)))
=> ([{} {}] [{} {}] [{} {}] [{} {}] [{} {}] [{} {}] [{} {}] [{} {}] [{} {}] [{} {}])

misha10:08:37

despite the :req

misha10:08:34

and it does not even complain, that :a/key spec is not defined.

misha10:08:11

next: I did try to follow the similar strategy for nested maps:

(s/def :company.department/employees (s/coll-of :company.department/employee))
and it becomes hairy very-very soon with very little nested structures. and unless you will use -un (which defeats whole purpose of namespaces in just few calls down the call stack) - your get-ins and update-ins become unreadable and unbearable

misha10:08:26

instead, I went with shorter namespaces, reflecting domain objects/classes, w/o including nestedness info into spec namespaces.

misha10:08:22

example above does not look too terrible, but it is really a hello-world grade one, and is already whole line long.

guy11:08:28

Thanks @misha really good to know

conan12:08:55

we have complex nested structures and don't really encounter a problem, but maybe we have more complex but less nesting - going down our tree, we quickly hit refs to other top-level entities, so maybe that keeps it ok. I always prefer having predictability over terseness, i don't really see much value in the latter.

👍 4
conan12:08:32

in that example above for instance, it's likely i wouldn't chooes to model an employee as something in the company hierarchy. i'd be more likely to have a :conan/company containing :conan.company/departments, and each of those containing lots of :conan/employees (rather than continuing the nesting).

4
conan12:08:31

or at the very least, if employees were in the company hierarchy, i'd look to break out of it and give each employee a :conan/person, i.e. a thing that exists independently of the company hierarchy but that can be attached.

conan12:08:41

The pattern where :my/a has a collection of :my.a/bs, each of which is a :my.a/b, which has a collection of :my.a.b/cs, each of which is a :my.a.b/c and so on works very well for us

conan13:08:36

but from a modelling perspective it's often useful to consider breaking that pattern when possible, so that a :my.a.b/c ends up with a collection of :my/zs

conan13:08:53

it's tricky to read this stuff without a concrete domain in mind

misha13:08:26

@conan I am not choosing terseness for the sake of terseness, I am suggesting to think twice before embedding "how one domain object is nested in another one maybe-only-in-just-one-out-of-many-existing-contexts" into spec namespaces

misha13:08:21

for example, if there is a code which manipulates some leaf of a deep nested structure outside of the nested structure context, does it really need to know that it is :foo.bar.baz.quux/fred and not just :quux/fred?

conan13:08:39

absolutely, gotta pick what works for you. everything we do is stored in datomic, so this is always our graphy structure

misha13:08:13

ofc, the easy escape would be :*-uning all the keys. but that is just robbing yourself of power namespaces gave you

conan13:08:18

in your example though, your function may well not need to know that - but as a developer, I do, and I don't like surprises

conan13:08:41

yeah unqualified specs are just for data coming from external systems imho

conan13:08:22

i'm definitely not saying this is the "right" way to do it, not even the only way - it's just intended as a useful starting point if you're unsure. I'll update the post to make that more clear

misha13:08:27

I agree with what you are saying about knowledge, but only in very limited set of usecases

misha13:08:41

and I'd say, those use cases are solved by namespaces for functions foo.bar.baz/quux-fred-manipulator vs e.g. quux/fred-manipulator (dont read too much into it)

conan13:08:11

really appreciate the feedback though

misha13:08:18

the same way as you can have your own update fn, more specific than clojure.core/update one, in another ns

misha13:08:51

with similar semantics, but with more knowledge about thing it sh/would be applied to so you-as-developer-would-know

conan13:08:07

yep, i can see that; we benefit a lot from transacting data generated from these specs into datomic, and it may colour my thinking more than i realise

misha13:08:29

oh, I tried keeping datomic db for such nested nss - was tough. and again - you are robbing yourself of a graph, replacing it with rigid tree

conan13:08:30

it's really quite astonishing that i can often write specs for a domain, write schema from those specs, and the generators will produce data that transacts first time into datomic; i don't think i've ever once come close to that level with other databases

conan13:08:46

we have a very graphy structure, and this works well with it

misha13:08:01

that is true, that potential is awesome!

conan13:08:49

essentially, anything that has isComponent ends up being another level of namespace hierarchy (i.e. :a.b.c/d), and anything that is not usually resets back to the top of the hierarchy (i.e. :e/f)

misha13:08:39

it is very close to that, yes, I agree. although company.department/employee, we crucified above - does not fit that opieop

conan13:08:31

yeah i'm terrible at examples, i'll think up a more evocative, less boring one over the weekend

misha13:08:44

that's fine, it is an example from linked article anyway

misha13:08:45

I have an example, contradicting deeply-nested isComponent rule: :geopoint/lat/long

bja14:08:37

is there anything like the plumbing.core/defnk (which used Schema) for spec?

snowell16:08:59

I have a function I'm trying to spec that takes 2 args, a map and a string that needs to be a key in that map. How do I reference the first arg in the spec for the second?

taylor16:08:14

@snowell you could use s/and with s/cat to check the args:

(defn foo [x y] (get x y))
(s/fdef foo
        :args (s/and (s/cat :x map? :y string?)
                     (fn [{:keys [x y]}] (contains? x y))))

snowell16:08:07

Yeah, I'd gotten there actually, but I was trying to get something that worked with generators, and apparently that's a lot more fickle

snowell16:08:25

Unless it happens to randomly generate the right string(s)

taylor16:08:42

hmm yeah because the OotB generators are created using just the first spec in the s/and

taylor16:08:26

but I don't think it'd be too hard to make a generator that will do this

snowell16:08:31

It's not the end of the world. Just playing with a new toy 🙂

taylor16:08:07

@snowell this might work:

(s/def ::foo-args
  (s/with-gen (s/cat :x map? :y string?)
              #(gen/fmap (fn [m] [m (rand-nth (keys m))])
                         (s/gen (s/map-of string? any?)))))

raymcdermott16:08:30

basic question on s/keys…

raymcdermott16:08:11

can I reuse the keys in q-min-n that I have defined in q?

taylor16:08:14

maybe spec/merge is what you need @raymcdermott. (s/merge ::q (s/keys :req [::min-n]))

4
💪 4
raymcdermott16:08:42

I’ll give it a try - thanks @taylor

👍 4
snowell16:08:29

@taylor It generated something, though it looks weird. It's really OK though, thanks anyway!