This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2019-01-08
Channels
- # announcements (6)
- # aws (1)
- # beginners (64)
- # boot (22)
- # calva (9)
- # cider (109)
- # clara (4)
- # cljs-dev (29)
- # clojure (112)
- # clojure-europe (2)
- # clojure-italy (6)
- # clojure-nl (3)
- # clojure-russia (215)
- # clojure-spec (80)
- # clojure-uk (13)
- # clojurescript (150)
- # code-reviews (3)
- # core-async (7)
- # cursive (37)
- # data-science (11)
- # datomic (76)
- # figwheel-main (6)
- # fulcro (56)
- # jobs (3)
- # jobs-discuss (22)
- # juxt (4)
- # off-topic (11)
- # pathom (16)
- # planck (5)
- # portkey (63)
- # re-frame (22)
- # reagent (3)
- # remote-jobs (1)
- # ring-swagger (5)
- # shadow-cljs (3)
- # testing (2)
- # tools-deps (6)
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
::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)
The idea is that contextually (in a specific map) a key may have a narrower spec than normal
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 %))
true
false))
(s/def ::parameters (s/map-of keyword?
(s/and
(s/keys :req [::kind])
(s/multi-spec parameter-kind ::kind))))
s/multi-spec
is kind of designed to use different specs based on data
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
.
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 https://github.com/clojure/spec.alpha ?we handle spec issues in the main CLJ jira system and I think there already is one for this
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...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.
actually I think your spec is wrong
youâre missing the top level args sequence
(s/fdef testfn :args (s/cat :m (s/keys :req-un [::type ::does-not-exist])))
that works for me
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.
you need to instrument again
or possibly unstrument / instrument (although I think either will work)
@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): https://dev.clojure.org/jira/browse/CLJ-2109
@alexmiller Could you please expand on the rationale behind this? https://dev.clojure.org/jira/browse/CLJ-2378?focusedCommentId=49601&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-49601 In https://groups.google.com/d/msg/clojure/RLQBFJ0vGG4/UGkYS7U_CQAJ you refer to the changelog, but that does not appear to include history data: https://github.com/clojure/spec.alpha/blob/master/CHANGES.md
So just repeating my comment in the jira, the idea behind instrument
is to check whether a function has been correctly invoked
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.)
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.
instrument has functionality to do stubs and mocking too, which can be used in combination with check
@urzds some examples of generative tests of fdefs: https://github.com/borkdude/speculative/blob/master/test/speculative/core_test.cljc
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?
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.
: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
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
(fn [args]
(s/assert args-spec args)
(let [res (calc args)]
(s/assert ret-spec res)
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)
ret)))
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.
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)
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
ah, it works with a var: https://clojuredocs.org/clojure.spec.alpha/get-spec
> 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: https://github.com/borkdude/speculative/blob/master/test/speculative/core_test.cljc#L107
Is it possible to spec this: #<Track: [nil :fiddle/links 1234]
â a tuple inside a deftype
deftypes do not expose their structure (other than the type)
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
so without more info, I would default to: no :)