Fork me on GitHub
#clojure-spec
<
2016-09-12
>
bret06:09:35

If I write a spec like:

(s/def :in/data (s/and
                  (s/keys :req-un [:in/id] :opt-un [:in/more])
                  (s/map-of #{:id :more} nil)))
My instinct is to not duplicate the keys and modify this to be:
(def req-keys [:in/id])
(def opt-keys [:in/more])

(defn unk
  "Returns ns unqualified keys for (possibly) qualified ones."
  [& ks]
  (map #(-> % name keyword) ks))

(s/def :in2/data (s/and
                   (s/keys :req-un req-keys :opt-un opt-keys)
                   (s/map-of (set (apply unk (concat req-keys opt-keys))) nil)))
which fails due to the nature of the s/keys macro. I understand the low level cause of the error. However, I want to make sure I’m not missing something fundamental about the intention. I did find this on the google group https://groups.google.com/forum/#!searchin/clojure/s$2Fkeys%7Csort:relevance/clojure/mlMYUrPVdso/ATklLgpGBAAJ so, possibly, I’m not completely alone in my instincts. But there was no meaningful reply.

jeroenvandijk07:09:05

I’ve found a case where conform -> unform -> conform leads to an invalid result. This is the case with the clojure.core.specs/defn-args spec. See https://gist.github.com/jeroenvandijk/28c6cdd867dbc9889565dca92673a531 Should I file a JIRA issue?

jmglov12:09:01

Sorry for asking such a basic question, but what is the recommended way to test spec'd functions in unit tests (i.e. by running lein test)?

jmglov12:09:39

I like the idea of combining unit and generative tests as per http://dpassen1.github.io/software/2016/09/07/from-repl-to-tests#a-better-way

jmglov12:09:16

But I'm not really sure how to get c.s.test/check to hook into the (deftest ... (checking ...)) style.

jmglov12:09:28

RTFM links welcome. 😉

otfrom12:09:23

jmglov: I'd be happy with figuring out that workflow too

tgk12:09:09

Yeah, I was puzzled with this as well. I ended up writing a very small namespace for it and creating a lein alias for running the specs

jmglov12:09:55

Using enumerate-namespace?

tgk12:09:40

The file is only on dev path by the way 🙂

jmglov12:09:49

Interesting. I didn't realise that test/check had a zero-arity form. Really useful!

jmglov12:09:36

Does it actually find all specs in all namespaces in your classpath, or something?

jmglov12:09:13

I don't see you specifying any namespaces under test, or requiring them in.

tgk12:09:16

Yes, as long as they have been evaluated

tgk12:09:34

Ah yes, that’s where refresh comes into the picture 🙂

jmglov12:09:42

Would your approach actually run all the specs for dependencies as well?

jmglov12:09:02

Or just stuff in your src?

jmglov12:09:15

That's nifty!

jmglov12:09:19

All the same, it would be great to find an approach that would allow me to drop spec generative tests into my standard clojure.test files.

jmglov12:09:46

I haven't found anything with Google, but stemming is really fighting me on this one. 😉

tgk12:09:00

Hmm yes, I don’t think specs for dependencies would be run. I can’t see how they would 🙂

jmglov12:09:15

That's good. 🙂

tgk12:09:17

I found it very hard to find anyone who’d hooked it into tests

jmglov12:09:40

So refresh simply evals everything in your source directories?

Alex Miller (Clojure team)12:09:26

using spec.test/check or spec.test/instrument will pick up any spec’ed fns that have been loaded and added to the registry, so it depends completely on what code you’ve loaded

jmglov12:09:29

Thanks, Alex!

jmglov12:09:52

Also, thanks for the spec Guide. I finished reading it, and it is really excellent!

Alex Miller (Clojure team)12:09:58

@bret I personally would prefer your first spec (although looks like you missing the kw namespaces on the map-of and you probably want s/merge instead of s/and)

jmglov12:09:47

OK, switching gears for a second, I'm obviously doing something silly, but I'm not sure what. I'm trying to spec out the input coming in from some JSON, and I have some code like this:

(ns wtf
  (:require [clojure.spec :as s]
            [clojure.spec.test :as test]))

(s/def ::contract_type_id pos-int?)
(s/def ::product (s/keys :req-un [::contract_type_id]))
(s/fdef exclude-products
  :args (s/cat :products (s/coll-of ::product)
               :excluded-contracts (s/coll-of ::contract_type_id))
  :ret (s/coll-of ::product)
  :fn #(<= (-> % :args :products) (-> % :ret)))

(defn- exclude-products [products excluded-contracts]
  (letfn [(excluded? [product]
            (some #{(:contract_type_id product)} excluded-contracts))]
    (remove excluded? products)))

jmglov12:09:46

My exclude-products function should do something this:

wtf> (exclude-products [{:contract_type_id 1} {:contract_type_id 2}] [1])
({:contract_type_id 2})

jmglov12:09:26

Things look good with exercise-fn:

wtf> (s/exercise-fn `exclude-products 1)
([([{:contract_type_id 2} {:contract_type_id 2} {:contract_type_id 2} {:contract_type_id 1}] [2 2 1 1 1 1 1 2 1])
  ()])

jmglov13:09:50

But check completely rejects my entire worldview:

wtf> (test/check `exclude-products)
({:spec #object[clojure.spec$fspec_impl$reify__14244 0x2c221658 "clojure.spec$fspec_impl$reify__14244@2c221658"],
  :clojure.spec.test.check/ret {:result #error {
 :cause "clojure.lang.PersistentVector cannot be cast to java.lang.Number"
 :via
 [{:type java.lang.ClassCastException
   :message "clojure.lang.PersistentVector cannot be cast to java.lang.Number"
   :at [clojure.lang.Numbers lte "Numbers.java" 225]}]
 :trace
 [[clojure.lang.Numbers lte "Numbers.java" 225]
  [wtf$fn__29911 invokeStatic "wtf.clj" 11]
...
  [java.lang.Thread run "Thread.java" 745]]},
                                :seed 1473685015567,
                                :failing-size 0,
                                :num-tests 1,
                                :fail
                                [([{:contract_type_id 2}
                                   {:contract_type_id 1}
                                   {:contract_type_id 1}
                                   {:contract_type_id 2}
                                   {:contract_type_id 1}
                                   {:contract_type_id 2}]
                                  [2 1 1 2 2 2 2 1 1 2])],
                                :shrunk {:total-nodes-visited 18, :depth 16, :result #error {
...

jmglov13:09:05

Can anyone shed light on what I'm doing wrong?

Alex Miller (Clojure team)13:09:42

@jeroenvandijk yes, please file a jira. this is where we are hurting for an s/vcat which Rich and I have talked about several times.

Alex Miller (Clojure team)13:09:36

@jmglov it looks to me like your :fn spec is wrong and should be comparing count of each thing?

jmglov13:09:51

@alexmiller Of course you are right. Thanks for pointing out my idiocy! 🙂

Alex Miller (Clojure team)13:09:14

well I wouldn’t go that far. :) fwiw, I’ve done the same.

jmglov13:09:05

Oh, so much better! Now summarize-results is showing me a bug in my code or spec. 🙂

bret13:09:27

@alexmiller I guess my point/observation is that the second one is not allowed at all. That was puzzling at first. I've missed s/merge all this time. I’ll look at that and see if that changes anything.

Alex Miller (Clojure team)13:09:24

@bret not sure what you mean by your point/observation, sorry

bret13:09:34

@alexmiller I probably shouldn't start writing at midnight on Sunday night. :) I guess it boils down to, is the reason this works

(s/keys :req-un [::k1 ::k2])
=> #object[clojure.spec$map_spec_impl$reify__13426 0x1b824394 "clojure.spec$map_spec_impl$reify__13426@1b824394”]
and this doesn’t
(def rks [::k1 ::k2])
=> #'onenine.core/rks
(s/keys :req-un rks)
CompilerException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol, compiling:(/Users/brety/dev/personal/onenine/src/onenine/core.clj:69:1) 
merely a consequence of the s/keys macro implementation or is this intended to not be valid?

Alex Miller (Clojure team)13:09:16

s/keys is a macro and expect a concrete list of keys

Alex Miller (Clojure team)13:09:23

so that’s as intended

Alex Miller (Clojure team)13:09:57

and the reason most of the spec creating fns are macros is to capture forms for reporting

jmglov13:09:20

Does anyone know how to make test/check work on private functions? I've tried #'full.namespace/foo, but I get java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Var.

jmglov13:09:46

Leaving off the #' tells me the function is not public, which I already know. 😉

Alex Miller (Clojure team)13:09:27

we might in the future have a fn entry point for s/keys with the caveat that you may lose some of the explain reporting capability

bret13:09:44

That is what I suspected (the reporting aspect) but wanted to confirm.

Alex Miller (Clojure team)13:09:45

@jmglov I don’t think that was considered in the design (and I’m not sure whether it should be)

jmglov13:09:10

Yeah, I'm a bit naughty, really.

lvh13:09:51

On a related note: I find myself regularly wanting validation of runtime-defined specs, where I learn e.g. the structure of the JSON in this particular REST API at runtime. This is for presumably obvious reasons, not very convenient right now.

jmglov13:09:58

This is always a tough call. I want to use private functions to communicate to my clients that they are not part of the interface, but I also want to be able to unit test them. 😕

lvh13:09:08

(Everything ends up being eval’d, which I guess is fine?)

bret13:09:33

It would be nice in some case, I think, to be able to define keys once and combine them in different ways ways when building specs. At least, as I think about repeating information in the spec(s) and trying to reduce that.

bret13:09:58

But I don’t have enough time in spec to be sure.

jmglov13:09:02

I might just go the foo.bar.internal route, where everything in the internal ns is public and can be tested, but is pretty clearly not for client consumption.

Alex Miller (Clojure team)13:09:27

@lvh yeah, I understand that as a use case, not sure how common that will be in general though

jmglov13:09:32

Using a namespace-level docstring to warn off potential troublemakers, of course.

Alex Miller (Clojure team)13:09:17

@bret well that’s exactly the point of having the registry

lvh13:09:17

alexmiller: If you give Clojure programmers a feature it seems like a matter of time before they’ll try to express as much of it as data, and then it’s not a long stretch until that data isn’t available at compile time 😉

lvh13:09:33

I might be able to get around it and just move more stuff into compile-time-land

Alex Miller (Clojure team)13:09:34

specs are data in s-expr form

Alex Miller (Clojure team)13:09:41

we haven’t released it yet, but I have a spec for spec forms

Alex Miller (Clojure team)13:09:16

(which revealed a lot of bugs in s/form :)

lvh13:09:20

nice; I would very much like that

lvh13:09:40

since a hypothetical awful person might want to construct specs at runtime and have better feedback about why they don’t work 😉

Alex Miller (Clojure team)13:09:10

oh, I don’t think you’re awful :)

Alex Miller (Clojure team)13:09:49

just not the primary use case we were working to support

bret13:09:44

@alexmiller Ok, this will help me.

;; I want to check that an input map's keys are valid
;;   where the keys are unqualified, some required, some optional,
;;   and not allow keys outside that set.

; This is straight forward
(s/def :in/data (s/and
                  (s/keys :req-un [:in/id] :opt-un [:in/more])
                  (s/map-of #{:id :more} nil)))

; but I'm (kind of) repeating information.
;
; If I write

(def req-keys [:in/id])
(def opt-keys [:in/more])

(defn unk
  "Returns ns unqualified keys for (possibly) qualified ones."
  [& ks]
  (map #(-> % name keyword) ks))

(s/def :in2/data (s/and
                   (s/keys :req-un req-keys :opt-un opt-keys)
                   (s/map-of (set (apply unk (concat req-keys opt-keys))) nil)))

; I have not repeated the key values but s/key doesn't allow it.
What is the proper way to write the spec where I not repeating information? I didn’t initially see a way to piece it together from ‘smaller’ specs since the args in map-of is really just a set used as a predicate.

bret13:09:08

I could be missing something fundamental.

Alex Miller (Clojure team)13:09:22

I’d say generally that Rich believes in open maps and that’s why this is not a feature provided out of the box

Alex Miller (Clojure team)13:09:35

and that I think your first example is what I would do if I was doing it

Alex Miller (Clojure team)13:09:56

(although nil is not a valid spec there - you want any?)

Alex Miller (Clojure team)13:09:14

and I would use s/merge instead of s/and

Alex Miller (Clojure team)13:09:45

which I think would gen better

bret13:09:41

So, s/merge can be used for combining more than s/keys (`s/map-of` in this case)?

Alex Miller (Clojure team)14:09:05

s/merge is used to combine (union) map specs

Alex Miller (Clojure team)14:09:38

it differs in not flowing conformed results like s/and and also in being better at gen'ing

bret14:09:50

I get the open map approach and generally like it. One thought I had, that really relates to the reporting aspect, is that s/keys supports ‘and’/‘or’ combinations of key vectors. So, keeping the form used for reporting as close to literal boolean expressions of literal key vectors is not a bad thing. Ok, thanks, this helps.

rickmoynihan14:09:56

what's the best way to spec that something satisfies? a protocol?

rickmoynihan14:09:11

obviously you can just use the (partial satisifies? Protocol) predicate... but is there a way for implementers to hook in and extend the generator to generate types that satisfy it? I could imagine that if implementers spec'd their constructing functions, you could get this almost for free.

jmglov14:09:39

Here's another fun one. Using the fixed version of the same spec as previously, I can use stest/check on it in my REPL:

kpcs.product-catalog.internal-test> (first (stest/check 'kpcs.product-catalog.internal/exclude-products))
{:spec #object[clojure.spec$fspec_impl$reify__14244 0x2c8e87a5 "clojure.spec$fspec_impl$reify__14244@2c8e87a5"],
 :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1473692045039},
 :sym kpcs.product-catalog.internal/exclude-products}

jmglov14:09:46

However, when I try to use it in a test, I get an exception. Here's what I'm trying to do:

(ns kpcs.product-catalog.internal-test
  (:require [clojure.spec.test :as stest]
            [clojure.test :refer [deftest is testing]]
            [kpcs.product-catalog.internal :as internal]))

(deftest exclude-products
  (testing "Specs"
    (let [res (first (stest/check 'kpcs.product-catalog.internal/exclude-products))]
      (is (= java.lang.String (type res)))))

jmglov14:09:54

And I get this:

java.util.concurrent.ExecutionException: java.lang.ClassCastException: clojure.lang.AFunction$1 cannot be cast to clojure.lang.MultiFn, compiling:(clojure/test/check/clojure_test.cljc:95:1)
 at java.util.concurrent.FutureTask.report (FutureTask.java:122)
    java.util.concurrent.FutureTask.get (FutureTask.java:192)
    clojure.core$deref_future.invokeStatic (core.clj:2290)
    clojure.core$future_call$reify__9352.deref (core.clj:6847)
    clojure.core$deref.invokeStatic (core.clj:2310)
    clojure.core$deref.invoke (core.clj:2296)
    clojure.core$map$fn__6856.invoke (core.clj:2728)
    clojure.lang.LazySeq.sval (LazySeq.java:40)
    clojure.lang.LazySeq.seq (LazySeq.java:56)
    clojure.lang.LazySeq.first (LazySeq.java:71)
    clojure.lang.RT.first (RT.java:682)
    clojure.core$first__6379.invokeStatic (core.clj:55)
    clojure.core/first (core.clj:55)
...

jmglov14:09:23

Any ideas?

jmglov15:09:30

To be clear, if I REPL into my kpcs.product-catalog.internal-test and run the code above, it works. If I run lein test, I get the exception.

jmglov15:09:47

I don't see what should be different between the two.

Alex Miller (Clojure team)15:09:16

that’s a problem with lein test’s monkeypatching

Alex Miller (Clojure team)15:09:30

it’s fixed in (not yet released) next version of test.check

Alex Miller (Clojure team)15:09:39

but you can disable lein monkeypatching to fix

Alex Miller (Clojure team)15:09:01

:monkeypatch-clojure-test false

jmglov15:09:12

Great, thanks!

Alex Miller (Clojure team)15:09:23

that will disable lein retest but otherwise should not affect what you’re doing

jmglov15:09:47

Just tried it, and it works perfectly.

Alex Miller (Clojure team)15:09:37

you are not the first person to encounter it :)

jmglov15:09:11

Thank goodness for that!

jmglov15:09:30

@otfrom @tgk Here's a cheap hack to fit check into my standard tests:

(deftest exclude-products
  (testing "Specs"
    (let [result (-> (stest/check 'kpcs.product-catalog.internal/exclude-products)
                     first
                     :clojure.spec.test.check/ret
                     :result)]
      (if (true? result)
        (is result)
        (is (= {} (ex-data result)))))))

otfrom15:09:24

jmglov: thx!

jmglov15:09:43

I'll make a checking function out of it and throw it in a test lib. The output is decent enough with the humane-test-output plugin. 🙂

jmglov15:09:26

If it's useful, you're welcome. Otherwise, I'm sorry for such a disgusting kludge! 😉

mike_ananev15:09:27

Hi there! Sorry for very dumb question. How to define spec for function with variable args?

bahulneel15:09:00

Hi guys, I've just started using clojure.spec and was wondering how to use clojure.spec.test/check with clojure.test

bahulneel15:09:46

sorry, missed the msg from @jmglov

Alex Miller (Clojure team)16:09:00

@mike1452 (s/fdef myf :args (s/cat :map-params (s/? map?))) will take both 0 and 1 (map) arg

Alex Miller (Clojure team)16:09:12

you can replace map? with something more specific too of course

bfabry17:09:46

it seems awkward that core/defn has a special syntax for arity dispatch but spec/fdef doesn't provide one for speccing/testing

bfabry17:09:40

doesn't really affect me as I never use arity dispatch but I could see it sucking if you previously used it a lot

Alex Miller (Clojure team)17:09:44

they are doing different things. most multi-arity functions share param definitions across arities and merging them works very nicely for this in most cases.

bfabry17:09:12

sure, for the :args, but it doesn't make the :fn for say map less readable?

Alex Miller (Clojure team)17:09:15

sure, although I think that’s an unusual case

bfabry17:09:14

true. I guess the common case would be reduce. smaller arity providing default value

ikitommi18:09:10

Is there a reason why if-let and when-let can’t return :clojure.spec/invalid?

hiredman19:09:22

well that is problematic

hiredman19:09:38

(if-let [a 1] '::s/invalid) works if you need a work around

ikitommi19:09:42

@hiredman cool, thanks. didn’t know that works too.

Alex Miller (Clojure team)20:09:01

there’s actually a ticket related to this

seancorfield21:09:10

I added the if-let example above to that ticket. I suspect people will run into this in more and more situations as they try to write conformers.

Alex Miller (Clojure team)21:09:59

and as there are more spec’ed things

ag21:09:52

I need a spec that would generate vectors of values taking random elements from a predefined list, e.g: [:foo :bar] [:foo] [:baz :bar] []… etc.

Alex Miller (Clojure team)21:09:22

(s/coll-of #{:foo :bar :baz} :kind vector?)

Alex Miller (Clojure team)21:09:33

you can also use the other options on coll-of to set :min-count, :max-count, :count constraints on the spec or :gen-max to cap what the generator will produce

ag21:09:43

thanks!