Fork me on GitHub
#clojure-spec
<
2019-01-08
>
lilactown00:01:53

AFAICT there's no way to do this without using :req-un 😞

favila00:01:56

https://gist.github.com/favila/ab03ba63e6854a449d64d509aae74618 is a hack I wrote a while ago that will add an additional thing to conform to for some of the named keys

favila00:01:11

spec is super duper opinionated on this point though

favila00:01:25

hence the complexity of the hack

favila00:01:57

use like (keys+ :req [::whatever] :conf {::whatever narrower-specish-thing})

favila00:01:00

::whatever value will be asked to validate against both its own spec and narrower-specish-thing (so ::whatever should be the widest possible spec you could have for that key)

favila00:01:13

but narrower-specish-thing will be used for conforming and generators

favila00:01:43

The idea is that contextually (in a specific map) a key may have a narrower spec than normal

favila00:01:05

which for some reason happens to me all the time and made spec very painful

favila00:01:30

the alternative is s/or with more predicates, and with-gen to adjust the generator

mattly00:01:33

you could also just forgo using s/keys and do it manually (spec/def :my-union/shape (fn [thing] (case (::kind thing) :bool (if (boolean? (::default thing)) true ::s/invalid) ::s/invalid)))

lilactown01:01:45

hm. yeah, I think I settled on:

(defmulti parameter-kind ::kind)

(defmethod parameter-kind :bool [m]
  #(if (boolean? (::default %))
     true
     false))

(s/def ::parameters (s/map-of keyword?
                              (s/and
                               (s/keys :req [::kind])
                               (s/multi-spec parameter-kind ::kind))))

Alex Miller (Clojure team)03:01:23

s/multi-spec is kind of designed to use different specs based on data

Alex Miller (Clojure team)03:01:03

you’d need to use it with s/keys and :req-un here though since you have the same attribute name with different specs apparently

urzds14:01:57

Back in December I asked whether it was possible to spec protocol methods, where the answer was "no" and "because of the implementation that is targetted at performance". I am wondering whether I could instead use multi-methods instead of protocols and methods and spec them. (My code should allow replacing the record with a map and the protocol methods with multi-methods.) Can I just use s/fdef on the multifn and spec :args and :ret that have to be valid for all implementations? An alternative would be to use pre/post conditions, but I cannot see anything resembling a pre-post-map (as is present for defn) in the docs for defmulti or defmethod.

Alex Miller (Clojure team)14:01:22

Currently, I do not believe that works, but I think it could be made to work

urzds14:01:57

I just tried the following code and got no error, which I guess suggests that it does indeed not work:

(require '[clojure.spec.alpha :as s])
(defmulti testfn :type)
(defmethod testfn :atype [m] 1)
(s/fdef testfn :args (s/keys :req-un [::type ::does-not-exist]))
(testfn {:type :atype})
; => 1
What would be the path forward from here? Should I open an issue / feature request for https://github.com/clojure/spec.alpha ?

Alex Miller (Clojure team)15:01:02

we handle spec issues in the main CLJ jira system and I think there already is one for this

Alex Miller (Clojure team)15:01:28

you didn’t call stest/instrument in the example above so that at least is a missing step

urzds16:01:06

You're right, the following code at least throws an exception:

(require '[clojure.spec.alpha :as s])
(require '[clojure.spec.test.alpha :as stest])
(defmulti testfn :type)
(defmethod testfn :atype [m] 1)
(s/fdef testfn :args (s/keys :req-un [::type ::does-not-exist]))
(stest/instrument `testfn)
(testfn {:type :atype})
=> clojure.lang.ExceptionInfo: Call to #'user/testfn did not conform to spec.
But it also throws this exception for arguments that should conform:
(testfn {:type :atype :does-not-exist 1})
=> clojure.lang.ExceptionInfo: Call to #'user/testfn did not conform to spec.
Sadly there is no explanation why...

Alex Miller (Clojure team)16:01:25

I don’t actually see a CLJ issue for just “multimethods can’t be instrumented” but would be ok to make one if you like! Like I said, I think this is something that is fixable.

Alex Miller (Clojure team)16:01:53

actually I think your spec is wrong

Alex Miller (Clojure team)16:01:03

you’re missing the top level args sequence

Alex Miller (Clojure team)16:01:37

(s/fdef testfn :args (s/cat :m (s/keys :req-un [::type ::does-not-exist])))

urzds16:01:35

Can specs be redefined? I just tried to execute (s/fdef testfn :args (s/cat :m (s/keys :req-un [::type ::does-not-exist]))) in the same REPL session, which appeared to be successful, but spec would still throw an exception even when I passed in the correct arguments. Only restarting the process fixed that.

Alex Miller (Clojure team)16:01:35

you need to instrument again

Alex Miller (Clojure team)16:01:13

or possibly unstrument / instrument (although I think either will work)

urzds16:01:30

yes, just calling stest/instrument again worked.

borkdude16:01:19

@urzds if you use something like component, you can hook up re-instrumentation with the start/stop lifecycles

urzds16:01:25

BTW, I also found the request for specs for protocol methods (my original question): https://dev.clojure.org/jira/browse/CLJ-2109

Alex Miller (Clojure team)17:01:57

So just repeating my comment in the jira, the idea behind instrument is to check whether a function has been correctly invoked

Alex Miller (Clojure team)17:01:18

The idea behind check is to verify that a function produces proper outputs in response to valid inputs

urzds17:01:01

Hm, I got that, but I would also like to see, during development, that a function is being invoked correctly and it responds correctly in live workloads. I.e. in my use-case I want more than to just check whether it was invoked correctly, but I also am not running the function in a test where I could use check. How is my use-case to be handled?

urzds17:01:34

What I did not understand is the technical necessity for instrument not to also check the return value. I assumed knowing why you decided against that might help me understand the context better.

borkdude17:01:30

@urzds instrument can take a performance hit when you have a lot of fdefs. if you check the return values, you will often check those twice, since they are arguments for another function. so I think it’s a sane default

borkdude17:01:30

this may not the reason that core decided to do this, but I have come to appreciate it for this reason

urzds17:01:54

In one of my cases these functions are GraphQL resolvers, invoked by Lacinia. So there really is nothing coming afterwards in my own code where I could check the return value... (And GraphQL schemas are not as expressive as Clojure Spec.)

borkdude17:01:29

@urzds you can try to write a generative test for it.

urzds17:01:47

Maybe my understanding of Spec is wrong, though. I thought I should spec everything that goes into my system and everything that goes out of it, in order to ensure that it behaves nicely with others.

Alex Miller (Clojure team)17:01:13

instrument has functionality to do stubs and mocking too, which can be used in combination with check

borkdude17:01:51

a ret and fn spec is really useful to check if your implementation is correct in combination with generative testing

borkdude17:01:04

@urzds you can always plug in the ret spec checking manually, if you want. just separate out the spec and call valid?

borkdude17:01:34

or use :pre and :post

urzds17:01:07

Hm, maybe I should find ways to split up those functions better in order to allow generative testing. Seems a bit difficult right now, because they rely on input from external services. So I just have unit tests for my internal functions, and wanted to rely on spec throwing exceptions when the interaction with the outside world shows signs of problems.

borkdude17:01:51

you can also use spec/assert

borkdude17:01:53

lots of options

urzds17:01:27

:pre and :post do not work here, because the functions are actually protocol methods (could be changed to multi-methods, but those also do not support :pre and :post).

borkdude17:01:27

spec/assert can also be elided with compile time options.

urzds17:01:53

spec/assert is what I am using right now. The whole body of the function is wrapped in spec/assert, which looks a bit ugly TBH and the commit introducing this will cause a large amount of code to be reformatted for indention, which is also undesirable.

borkdude17:01:40

then don’t wrap. you don’t have to. and when you elide the call, you’ll be left with an empty function

urzds17:01:40

How would I not wrap and still check the return value, in the absence of :post?

borkdude18:01:03

@urzds

(fn [args]
  (s/assert args-spec args)
  (let [res (calc args)]
    (s/assert ret-spec res)
    res))

urzds11:01:52

Another recommendation I received was something along these lines:

(defn wrap-fn-with-spec [f s-args s-reg]
  (fn [& args]
    (s/assert s-args args)
    (let [ret (apply f args)]
      (s/assert s-ret ret)
      ret)))

borkdude11:01:23

yeah same idea. you could also do it with a macro to receive better line errors

urzds11:01:49

Is there a way to retrieve the specs attached to the symbol using s/fdef? That might make the above function integrate a bit better with the rest of spec.

borkdude11:01:16

yes, (s/get-spec foo)` and then call :args or :ret on it.

borkdude11:01:56

but another option is to spec the args and ret spec separately, so you can write a more specific generator for it

urzds11:01:13

So like this?

(defn wrap-fn-with-spec [f]
 (let [s-fn (s/get-spec f)
       s-args (:args s-fn)
       s-ret (:ret s)]
   (fn [& args]
     (s/assert s-args args)
     (let [ret (apply f args)]
       (s/assert s-ret ret)
       ret)))

borkdude11:01:54

I’m not sure if s/get-spec works with a function, I don’t think so. so you’ll need the symbol for the function too

borkdude11:01:03

just try it from the REPL and you’ll see.

borkdude11:01:36

so you’re better off passing the var then

urzds11:01:48

> but another option is to spec the args and ret spec separately, so you can write a more specific generator for it How do you mean? Doesn't s/fdef have the args and ret spec separately already?

borkdude11:01:39

yes, but they don’t have a specific name, so you cannot generate specific combinations of arguments. here’s an example for assoc: https://github.com/borkdude/speculative/blob/master/test/speculative/core_test.cljc#L107

borkdude11:01:58

I’m not saying you have to, but it gives options

urzds17:01:31

Guess I gotta read more docs about this subject.

Dustin Getz18:01:51

Is it possible to spec this: #<Track: [nil :fiddle/links 1234] – a tuple inside a deftype

Alex Miller (Clojure team)19:01:26

deftypes do not expose their structure (other than the type)

Alex Miller (Clojure team)19:01:50

whether you can spec it in some other ways depends on what interfaces/protocols you implement and which predicates you want to spec it with

Alex Miller (Clojure team)19:01:26

so without more info, I would default to: no :)