Fork me on GitHub
#clojure-spec
<
2018-02-27
>
borkdude12:02:33

I have written a macro to get rid of some boilerplate. I’m using a spec to validate arguments passed to a generated function.

(s/merge ::util/job-args
           (s/keys :req-un
                   [::importers
                    ::database
                    ::rabbitmq
                    ::stats]))
But I still have to destructure the args, where something could go wrong if you make a typo.
(let [{:keys [report importers database rabbitmq stats]}
        args]
    )
Is there a good solution for this, e.g. key destructuring using spec?

gfredericks12:02:21

I'd probably use plumbing/safe-get

borkdude12:02:22

yeah, I thought about writing that before I wrote the specs… might be just as effective

borkdude12:02:54

but nice to have it in a lib

misha15:02:23

well, it has nothing to do with spec opieop

borkdude16:02:13

but it does what I want. 😄

gfredericks16:02:25

theoretically it overlaps with spec because the spec could tell you whether you're accessing a key that can be ex-spec-ted to be there

stathissideris16:02:32

@borkdude I’ve written this https://github.com/stathissideris/spectacles but it doesn’t cover destructuring

stathissideris16:02:58

it has get, get-in, assoc, assoc-in, update, update-in

stathissideris16:02:21

you can also compose the lenses

misha16:02:26

@gfredericks should it, say, throw, if you are asking for key not in spec?

gfredericks16:02:45

@misha that sort of thing, yeah

misha16:02:27

then it'd be nice app-level let-speced macro

stathissideris16:02:41

that’s what spectacles does! (more or less)

stathissideris16:02:04

if someone wants to extend it with let-speced pull requests are very welcome

misha16:02:45

I think you are missing binding part of destructuring, @stathissideris

misha16:02:17

this is where the money is

stathissideris16:02:35

definitely, but there is quite a bit of code there to introspect the specs etc

stathissideris16:02:07

so it’s a good base to build on, that’s what I’m saying 🙂

misha16:02:45

I think the most common and useful case would be specced-destructuring for maps (s/keys)

misha16:02:28

because most of conformed things end up being maps anyway opieop

stathissideris16:02:03

the main reason I didn’t do it was the syntax, couldn’t come up with a nice way to express it

misha16:02:06

and seq-destructuring does not need any names from input value anyway

stathissideris16:02:21

because the spec name has to be mentioned

stathissideris16:02:09

it’s already a bit awkward with get-in

misha16:02:49

I'd love it to have let-like form

stathissideris16:02:04

@misha can you come with an example of what it would look like using it?

misha16:02:10

not yet, thinking of options. what I know, is: - binding has to be a core feature, to avoid repetition - should look "familiar", so plugins, like #cursive could understand it (most probably like let) - should be natural to use, because following does not really looks good:

(let [_ (spec-bind spec m [foo bar baz])]
  ...)

stathissideris16:02:18

how about:

(lens/let [{::logging [::level ::destination ::type]} foo]
  (prn "level is" level)
  (prn "type is" type))

misha16:02:19

but that is second best of my ideas (but I dont have top1 yet :) )

stathissideris16:02:39

instead of :keys you give the name of the spec

stathissideris16:02:54

(haven’t worked out the implications of the syntax yet)

misha17:02:46

this is better, although preserving all destructuring features would be neat

misha17:02:03

(not saying you can't have both with this design, just need to think it all through)

stathissideris17:02:58

yeah, the ideal situation would be for spectacles to handle the “special” parts and pass on anything not related to spec to clojure.core/let

misha17:02:40

up next: how to bind keys from the same map using different specs? 2 lines? or 2 specs in destructuring form?

stathissideris17:02:36

(lens/let [{::storage [::url ::port ::password]} foo
           {::logging [::level ::destination ::type]} foo]
  (prn "level is" level)
  (prn "type is" type))

misha17:02:42

select-keys should just work for this (passing the rest to core/let)

stathissideris17:02:31

hmm, it will throw if you try to destructure a key that is not speced, but not when the map contains extra keys

misha17:02:25

a week ago we were discussing adding & to the map destructuring, so I spent some time looking at clojure.core/destructure

borkdude17:02:30

actually my problem was like this:

my-ns=> (s/def ::job-args (s/keys :req-un [::report ::scheduler]))
:my-ns/job-args
my-ns=> (lens/get {:scheduler 1} ::job-args :scheduler)
1
my-ns=> (lens/get {:foo 1} ::job-args :scheduler) ;;=> nil
So the input map should conform to the spec, which I can validate with e.g. s/assert, s/valid? etc.

borkdude17:02:15

but you can also get the keyword wrong of course, this is what spectacles checks

stathissideris17:02:46

@borkdude are you saying that the last call should throw instead of returning nil?

misha17:02:55

I think the value proposition here would be, "(say) throw, if trying to bind value under key not in spec"

borkdude17:02:26

both things can go wrong: your input map or your selection in the map

misha17:02:51

if we are still on let-like macro topic: I'd just check if the key is supposed to be in map according to spec, because validation can be expensive, and I'd like to be able to separate the two

misha17:02:17

although it starts to sound like static types opieop

borkdude17:02:56

safe-get is the most effective in my case here: I just get an exception and prints out the keys that were in the map and the key I used. I can figure out which one was wrong

borkdude17:02:35

I guess you can mess up specs themselves too, nothing protects you from typos… typos all the way down

misha17:02:10

from a broader perspective, destructuring and select-keys - are a bit orthogonal to each other, and both would need to be addressed individually

stathissideris17:02:14

⌨️ :thunder_cloud_and_rain:

stathissideris17:02:42

@misha select-keys would be a good (and easy) addition to spectacles

borkdude17:02:07

I just discussed select-keys with someone else. After you use select keys you can still mess up selecting from the result of select keys

stathissideris17:02:29

not if you’re using lens/get! 🙂

misha17:02:54

@borkdude this is why I want let :)

misha17:02:35

I much more often (destructure) bind keys, than select-keys and then bind

stathissideris17:02:33

would be good to work out a good syntax for this

misha17:02:52

if collision with :my-ns/keys can be solved - it is the best syntax there could be, already

mpenet17:02:28

Personally I would just add a :spec key in the map destructuring form and limit it at that: {:as foo :keys [..] : spec ::foo}

stathissideris17:02:30

just don’t ever call your specs ::keys 😂

misha17:02:00

another data point, i usually don't use keys in :keys, but symbols (let [{:my.ns/keys [foo bar]} m])

stathissideris17:02:01

@mpenet that does sound a bit less collision prone, thank you!

mpenet17:02:26

Would work for seq destructuring too

mpenet17:02:56

[..... :as foo :spec ::foo]

misha17:02:57

@mpenet you then miss on opportunity to destructure by multiple specs in the same map :)

mpenet17:02:17

Yes true, trade-offs

stathissideris17:02:27

@mpenet I’ve added your idea to the issue

stathissideris17:02:32

with attribution!

misha17:02:35

you probably don't need spec-destructuring for seqs

mpenet17:02:01

Prolly not

misha17:02:06

unless you are willing to support spec form walking for nested specs like s/or

mpenet17:02:34

Spec walking is gross

misha17:02:35

then OMG you'd solve "damn, I forgot which keys I used in s/or for this"

mpenet17:02:50

Too alpha to play this game imho

mpenet17:02:07

Necessary evil for now

misha17:02:10

2 years strong alpha kappa

mpenet17:02:57

I am hoping we ll get something better for introspection/composition

stathissideris17:02:39

spectacles relies on looking at forms to figure out the keys

stathissideris17:02:51

but yeah it feels risky

misha17:02:13

at this point I'd like to have compositional spec-walking

misha17:02:51

it seems like everyone does it over and over again, for spectacles, for expound, etc.

mpenet17:02:41

Yup, spec-tools, speculate, etc been there done that too and trying to avoid doing it from now on;)

stathissideris17:02:12

spec-based seq destructuring could check that (1) the spec is in fact a coll-of, a cat or a tuple (2) for finite length specs, you’re not attempting to destructure more elements than what is defined in the spec

stathissideris17:02:22

not sure how useful…

stathissideris17:02:18

sorry I’m not sure what that emoticon means!

stathissideris17:02:26

@misha thanks for the comment

misha17:02:23

I went for "not really useful"

stathissideris17:02:24

yeah maybe not really worth it

misha17:02:33

@stathissideris you need to recursively call s/form until you get actual form back, here (and pretty much everywhere where you call s/form) https://github.com/stathissideris/spectacles/blob/master/src/spectacles/impl.clj#L38

misha17:02:28

oh wait, is this fixed in spec already, sweet