Fork me on GitHub
#clojure-spec
<
2018-09-02
>
bbloom18:09:00

what do people do when you have different keys for different specs, but want to handle them generically? do you just use an extra key to index back in to that map? for example, i’m trying to model a WebAssembly AST, which has “modules” which are made up of “sections” and each section has a vector of “fields”. but while fields have common structure across types of sections, i’d like to have ::funcs and ::types and ::imports instead of just ::fields — my least bad idea is to add (s/def ::fields-key #{::funcs ::types …}) and then indirect through that

bbloom18:09:34

the two other more bad ideas are 1) just use the abstract name ::fields and rely on the spec’s global name validation. This isn’t ideal because (A) it fails to spec the fact that fields in the funcs section must conform to ::func and (B) precludes unname-spaced keys, which is what i’m actually doing & would have to do a much bigger refactor for that

bbloom18:09:22

and 2) just copy the fields to the other name like (let [{:keys [fields]} ast, ast (assoc ast ::funcs fields)]

bbloom18:09:26

this is really nice

bbloom18:09:31

but it makes rewrites basically impossible

bbloom18:09:40

b/c if you edit ::funcs, then ::fields is stale

bbloom18:09:16

is this problem explanation clear? if not, why not? if so, what do you guys do about this sort of problem?

jeaye18:09:48

> what do people do when you have different keys for different specs, but want to handle them generically? We'll define it somewhere common, like (s/def ::field blah?) and then alias it to match the expected keys where it's used like (s/def ::my-field ::common/field). > ... while fields have common structure across types of sections Depending on how common they are, you may define a (s/def ::base-field (s/keys :req [...])) and then do a (s/def ::field (s/merge ::common/base-field (s/keys :req [...]))) in the namespace tied to that specific field.

bbloom18:09:52

yeah - so that much i’ve mastered. the tricky part is the collection of these things. for example:

bbloom18:09:41

(s/def ::funcs-section (s/merge ::section (s/keys :req [::funcs]))

bbloom18:09:00

given: (s/def ::section (s/keys :req [::fields]))

bbloom18:09:16

now some general code wants to visit all fields in all section

bbloom18:09:31

how do you define ::fields?

bbloom18:09:54

assuming (s/def ::funcs (coll-of ::func) and (s/def ::func (s/merge ::field ...))

bbloom18:09:29

my idea is to do: (s/def ::section (s/keys :req [::fields-key])) and then indirect through that key

bbloom18:09:36

basically make the polymorphism explicit

jeaye18:09:31

What about: (s/def ::funcs-section (s/merge (make-section-spec ::my-field-1 ::my-field-2) (s/keys :req [::funcs]))

bbloom18:09:56

the challenge isn’t making the spec - it’s using the data that has been spec’d

bbloom18:09:13

i want to be able to write (visit-section-fields some-section-of-any-kind)

bbloom18:09:51

where visit effectively does something like: (update-in section [:fields] #(mapv f %))

bbloom18:09:03

my “least bad idea” from above is to do:

jeaye18:09:04

Yeah, that's beyond me. spec-tools allows for better introspection though. Might be of help.

bbloom18:09:10

(update-in section [(:fields-key section)] #(mapv f %))

misha19:09:15

@bbloom can you share a snippet of an ast? as json or whatever.

👍 4
bbloom19:09:47

there’s a bunch of different designs i’m experimenting with, but it basically is something like:

bbloom19:09:09

{:type-section {....}, :funcs-section {....}, :exports-section {....}} where each of those are something like {:env {$someid 0} :funcs [....

misha19:09:18

while you are at it, how about multimethod for visit-section-fields?

bbloom19:09:50

@malch the logic for visit-section-fields is trivially monomorphic if a section is defined as {:env ..., :fields [...]}

misha19:09:51

so you want same field name but different spec forms for it?

bbloom19:09:20

honestly, i don’t want different field names at all - i’m happy to have them all called :fields, but that makes the specs less strict

misha19:09:37

will you work with qualified keys in data itself? or unqualified?

misha19:09:06

if unqualified – you just need the same (name kw) for those different specs: :foo/bar :baz/bar

bbloom19:09:06

i have no need for extensibility, so i was hoping to use unqualified keys to just avoid that entire syntactic headache

misha19:09:01

:wasm.type/fields, :wasm.funcs/fields

misha19:09:25

there is also (s/keys :req [(or :foo/bar :baz/bar]), but I am not really sure what the behavior contract there

bbloom19:09:08

> :wasm.type/fields, :wasm.funcs/fields i don’t understand what you’re trying to say with this

misha19:09:34

I think I did not understand your initial question and examples opieop

bbloom19:09:12

ok - new least-bad idea: don’t try to spec my whole AST - just spec pieces of it on an ad-hoc as needed basis ¯\(ツ)/¯ take advantage of a-la-cart, i guess

bbloom19:09:37

i need to write a validation pass anyway

bbloom19:09:38

my experience with spec seems to be 1) it forces me to consider my designs & often improves them and 2) then doesn’t really add much actual value for checking at all

☝️ 4
misha19:09:50

how do you intend to use spec for this?

misha19:09:20

gen-testing? inbound data validation? destructuring?

bbloom20:09:53

i was hoping to use it to validate the inputs/outputs of each compiler phase by just conforming the root node of the tree - but i need to hand-craft validation logic anyway, so i’ll use spec piece meal in there if it makes sense