Fork me on GitHub

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

favila00:01:56 is a hack I wrote a while ago that will add an additional thing to conform to for some of the named keys


spec is super duper opinionated on this point though


hence the complexity of the hack


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


::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)


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


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


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


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


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)))


hm. yeah, I think I settled on:

(defmulti parameter-kind ::kind)

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

(s/def ::parameters (s/map-of keyword?
                               (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


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


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 ?

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


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])))


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)


yes, just calling stest/instrument again worked.


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


BTW, I also found the request for specs for protocol methods (my original question):

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


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?


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.


@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


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


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.)


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


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


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


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


or use :pre and :post


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.


you can also use spec/assert


lots of options


: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).


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


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.


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


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



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


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)


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


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.


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


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


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)


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


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


so you’re better off passing the var then


> 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?


yes, but they don’t have a specific name, so you cannot generate specific combinations of arguments. here’s an example for assoc:


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


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 :)