Fork me on GitHub
#clojure-spec
<
2021-05-18
>
donavan16:05:01

Is there a way to do this:

(s/def ::bar #{:one :two :three})
(s/def ::foo
  (s/with-gen
    (s/keys :req [::bar])
    #(gen/fmap
      (fn [foo]
        (assoc foo ::bar :one))
      (s/gen ::foo))))
without having to create temporary specs like this due to the spec lookup loop
(s/def ::bar #{:one :two :three})
(s/def ::foo*
  (s/keys :req [::bar]))
(s/def ::foo
  (s/with-gen
    ::foo*
    #(gen/fmap
      (fn [foo]
        (assoc foo ::bar :one))
      (s/gen ::foo*))))

Alex Miller (Clojure team)16:05:13

You can gen recursive specs

Alex Miller (Clojure team)16:05:57

Can you explain why you need with-gen?

donavan16:05:21

::foo is actually a multi-spec and only the one value of ::bar is valid for this implementation

donavan16:05:56

So without tweaking the generator it fails to generate valid examples

Alex Miller (Clojure team)16:05:25

Have you looked at the generator override map?

donavan16:05:30

By “You can gen recursive specs” do you mean create the s/def calls dynamically in a macro?

Alex Miller (Clojure team)16:05:02

No, I mean you can have recursive specs and call gen on them

Alex Miller (Clojure team)16:05:32

I think I did not get what you were trying to do

donavan16:05:36

Haha, so it actually is both ultimately a multi-spec and a recursive one. The recursion ‘within’ the generator works fine if I split the spec into the ::foo* and ::foo defs

donavan16:05:17

It’s just the call to s/gen within the s/with-gen that loops expectedly

donavan16:05:51

It’s being fed into another library that does the generating so I can’t supply the override map

Alex Miller (Clojure team)16:05:33

What’s the point of the initial gen if you just override :bar?

3
Alex Miller (Clojure team)16:05:51

Aren’t you always generating the same map?

Alex Miller (Clojure team)16:05:01

If so, just (s/gen #{ {::bar :one} })

Alex Miller (Clojure team)16:05:30

Or gen/return if that is easier to read

donavan16:05:31

I’m not I follow exactly; the ::bar spec is used in all the multi-specs it’s just that some of the types it’s values are restricted

Alex Miller (Clojure team)16:05:52

Is this simplified and there are other keys too?

donavan16:05:04

Oh, yes sorry this is rather simplified!

Alex Miller (Clojure team)16:05:22

You left out half the problem and half the constraints

Alex Miller (Clojure team)16:05:20

It may not actually be in an inifinite loop - it may just be making very big intermediate colls

donavan16:05:48

The simple case above loops forever

donavan16:05:35

Apologies 🙂 I was just trying to ask the simplest question. I could mull it over some more…

Alex Miller (Clojure team)16:05:07

If you have any coll-of specs, make sure you set :gen-max 3 on them

Alex Miller (Clojure team)16:05:38

There are also some spec dynvars controlling recursion, might want to check those too

Alex Miller (Clojure team)16:05:15

All that said, there are some outstanding bugs in this area and it’s inherently tricky

donavan17:05:16

Scratching a little deeper the loop I’m referring to is within the implementation, as spec is currently implemented you cannot define a generator ‘in terms of itself’ like I do above for any of the implementations such as map-spec-impl . It’s deeply baked into the implementation so I suspect it won’t change even though at first glance that seemed like a natural way to scratch my itch 😄 . I’ll re-evaluate what I’m trying to do, thanks @alexmiller

Jeff Evans18:05:29

does it make sense to call instrument from a clojure.test fixture? https://clojuredocs.org/clojure.test/use-fixtures ex: if we have tests for various namespaces, each of which should really instrument the specs as they run

Alex Miller (Clojure team)18:05:18

and unstrument at the end :)

👍 3
Jeff Evans20:05:56

nice, thanks. will have a look

Jeff Evans21:05:09

how about this situation? I want b to be an optional argument to my-fn, but if provided, then it should match some predicate (like map?)? this doesn’t quite work.

(defn my-fn
  [a & [b]]
  ;; do stuff with a and b
)

(s/fdef my-fn
        :args (s/cat :a int? :b map?)
        :ret  map?)

Jeff Evans21:05:25

(my-fn 1)
Execution error - invalid arguments to user/my-fn at (form-init6149560983531223969.clj:1).
() - failed: Insufficient input at: [:b]

Alex Miller (Clojure team)21:05:58

you spec'ed it as a function taking 2 args (int and map)

Alex Miller (Clojure team)21:05:34

you can use s/? to spec an optional part of a regex op

Alex Miller (Clojure team)21:05:34

it's a little unclear to me if you meant [b] or b in the my-fn definition

Alex Miller (Clojure team)21:05:20

I guess you mean what you said

Alex Miller (Clojure team)21:05:34

and :args (s/cat :a int? :b (s/? map?)) should work for int and optional map

Jeff Evans21:05:19

awesome. thank you, Alex!