Fork me on GitHub
#clojure-spec
<
2019-02-27
>
Audrius14:02:41

Can I have spec for a map entry that it has keyword, but corresponds to a different spec? I have such map {::value "" ::other-map {::value {::bla 1}}} so my ::value sometimes is string sometimes is map...

favila15:02:03

No. Spec is philosophically opposed to this distinction: https://clojure.org/about/spec#_map_specs_should_be_of_keysets_only

favila15:02:30

You could write your own spec form (hard) but it will never be built in.

favila15:02:17

I wrote one out of necessity, but I don't necessarily recommend using it: https://gist.github.com/favila/ab03ba63e6854a449d64d509aae74618

favila15:02:18

My spec isn't going to help in your case

favila15:02:31

If you can, you should change your map to use a different keyword

favila15:02:55

or, it's possible you should be specing a higher-level language

valerauko15:02:33

why would you want to do that?

Audrius15:02:56

I amended my question...

favila15:02:12

@vale Real World Data (tm) has fields whose type may change depending on what map it is in

favila15:02:48

I appreciate and understand the philosophical stance of spec here, but it does cause friction

favila15:02:56

The case I always seem to run in to is a field which has a baseline type (e.g. it should be number?) wherever it appears, but in certain kinds of maps (defined with s/keys) it must be some subset of its allowed values (e.g. even?)

valerauko16:02:37

you could namespace those specific use cases

valerauko16:02:06

like :map-with-strings/name and :map-with-whatevers/name

valerauko16:02:21

and then :key-un to ignore the namespace

valerauko16:02:12

::outer-map/value string? ::inner-map/value (s/keys ...)

valerauko16:02:38

that is imo the most spec-friendly way to say "a value as in outer-map and value as in inner-map"

favila16:02:10

yeah, so basically put a transformation in front of all these functions

favila16:02:31

like I said, friction

favila16:02:35

@audrius if you control the keys and the shape of your data, you should use different keys (even just namespaces) as @vale suggests

❤️ 5
favila16:02:25

the spec philosophy of "one key one type" is a worthy goal

valerauko16:02:27

no, you don't have to have different keys in your data

valerauko16:02:40

just spec them in different namespaces

favila16:02:54

this forces the concrete maps to use un-namespaced keys

valerauko16:02:10

why would you have two names in the same namespace refer to multiple different things

valerauko16:02:56

you're right i don't know your use case

favila16:02:58

it's pretty easy to encounter data shaped like this

favila16:02:03

e.g. datomic transaction maps

valerauko16:02:12

i'll let smarter people argue the point

lilactown16:02:04

An example would be like we are trying to spec a user’s medical records. They have a coverage type: :coverage/type that can be #{:medical :dental :vision} and then in a certain context, you want to specify it must be :medical along with some other facts about the user’s data

favila16:02:32

that is exactly the case I encounter constantly

favila16:02:42

but it is different from the OP's case which is more extreme

favila16:02:00

here's an extreme example for a datomic transaction map: {:db/id (string, or tempid-record, or entity-id-long, or keyword-ident, or tuple[entity-id-long or keyword-ident, indexable-datomic-valuetype)}

favila16:02:47

and suppose for a particular function you want to specify that no tempids are legal for :db/id

favila16:02:58

you can't respec :db/id contextually

favila16:02:12

you can add a predicate, but you have the burden of maintaining the predicate, a less useful s/merge, and custom generators

favila16:02:56

or, you can make a new field and remap key names on the way in

favila16:02:11

(i.e. transform your data for typing reasons)

lilactown16:02:00

yeah. atm we have a specific medical-coverage? predicate that we combine with others. it’s not the greatest

favila16:02:36

That gist I linked earlier may help you

favila16:02:50

it was written exactly for the use case you described

favila16:02:54

(keys+ :req [:coverage/type] :conf {:coverage/type #{:medical}}) would produce an s/keys spec where :coverage/type was checked both against its natural spec and any override you have in :conf, and :conf is the one used for generators

lilactown16:02:28

that's awesome! thanks, I'll check it out

drone16:02:43

for things related (but not the same) to this, we use different records with specs. the records may have overlapping keys, and records of different kinds can be determined by their record (really, class) type

hiredman21:02:25

that sort of sounds like a multi spec

hiredman21:02:43

keyed on :coverage/type

lilactown21:02:20

the coverage type might be 1 part of many keys

lilactown21:02:34

ex: {:coverage/type :medical :coverage/code 1234 :coverage/status :active} answers "does user have XYZ program", for example

drone23:02:40

multi-spec uses a multi-method to retrieve the correct spec based on whatever dispatch function you choose. so you could dispatch on the value of :coverage/type and return different specs for each of your coverage record “types”

favila23:02:08

that's not the goal here

drone23:02:22

there seem to be at least two different things being discussed

drone23:02:27

what is the goal

favila23:02:41

Will and I were discussing the situation where we want to spec a map where an entry is only allowed some subset of what is normally allowed

favila23:02:06

:coverage/type doesn't alter the type of the map (what multispec would allow)

favila23:02:45

this is "my function takes coverage maps but only :medical ones"

favila23:02:44

there's also an open vs closed tradeoff here

favila23:02:54

multispec can only be open

drone23:02:41

> spec a map where an entry is only allowed some subset of what is normally allowed you mean a coverage map tagged as :medical can only contain some subset of all coverage map keys?

favila23:02:49

we mean there's a map type "coverage", one of its keys is coverage/type which can be #{:medical :dental} this function only deals with :coverage/type = :medical

drone23:02:01

and spec’ing the function with medical-coverage? (mentioned above) is too much work?

drone23:02:10

given this data model, I’m not seeing how else you’re proposing to check this aside from looking at the tag?

favila23:02:11

(s/and ::coverage #(= (:coverage/type %) :medical)) you mean?

favila23:02:18

this is awkward is all

favila23:02:32

you need to override the generator too

favila23:02:44

you need to chain a bunch of one-off predicates if you have more subset constraints

favila23:02:02

it would be nice to override in s/keys directly

favila23:02:11

I wrote something hacky that does this

favila23:02:41

(keys+ :req [:coverage/type] :conf {:coverage/type #{:medical}}) is what it looks like

favila23:02:00

the spec in the :conf map is used for conforming and generation

favila23:02:14

(oh, also you can't s/merge s/and)

favila23:02:51

another alternative is key rewriting

favila23:02:51

:coverage/type -> :coverage/type=medical, then you can (s/keys :conf [:coverage/type=medical), but that just shifts the pain around

drone23:02:34

so, why not just have specs to represent each of the kinds of coverage: medical-coverage, dental-coverage, ... and then a type that represents the common, generic coverage?

drone23:02:56

s/type/spec

favila23:02:05

the constraint is, :coverage/type is the concrete key

favila23:02:18

when you have a coverage map, it must have :coverage/type as the key

favila23:02:27

but different functions want different values for that key

favila23:02:56

so you can either chain predicates to your s/key with s/and (and lose niceties like s/merge, automatically correct generators, fewer predicates), or you can rewrite your key so that type and value match (and end up with key rewriting layers everywhere)

favila23:02:21

what you cant do is say "for this keys spec, only allow some of what would normally be in :coverage/type"

lilactown23:02:08

@mrevelle the point is that I don't actually care about the coverage type. what I do care about is answering some higher-level question about the data

lilactown23:02:39

like I might want a spec that is called :user/super-cool-program

drone23:02:24

@favila ah, thank you. I am tracking now

lilactown23:02:28

and their coverage map should look like:

{:coverage/code 1234
 :coverage/type :medical
 :coverage/status :active}

favila23:02:46

S/keys blurs the lines between a spec and a predicate somewhat

lilactown23:02:07

the code, type and status tell me that the user has "Super Cool Program"

favila23:02:55

It’s a fancy composable predicate system for map-shaped data

favila23:02:56

But you either obey its “spec key = type” constraint or you lose all the niceties it provides