Fork me on GitHub
#clojure-spec
<
2019-02-13
>
caleb.macdonaldblack06:02:04

Is there a way to create an anonymous spec? So instead of defining it, I can just create one and pass it into spec functions?

seancorfield06:02:43

@caleb.macdonaldblack I suspect the answer is "yes" but it's different between spec1 and spec2...

caleb.macdonaldblack06:02:17

spec2 as in the next version of spec?

seancorfield06:02:23

In spec1, you can mostly use a predicate interchangeably with a spec. In spec2, you can construct spec objects on the fly.

seancorfield06:02:36

Yes, clojure.spec-alpha2

caleb.macdonaldblack06:02:50

Well I'm now excited for spec2

seancorfield06:02:19

Hahaha... well, there are no releases yet, but if you're using deps.edn, you can test against it.

caleb.macdonaldblack06:02:39

I'm using leiningen. I may as well wait until an official release. I'm glad to see spec moving in that direction

seancorfield06:02:06

Have you seen Rich's talk from Conj? (Maybe Not)

seancorfield06:02:20

He talks about future direction for spec...

caleb.macdonaldblack06:02:19

if so I haven't seen it. I'll have a look though

seancorfield06:02:51

Specifically he talks about how s/keys complects the shape of data and the actual checks that are needed in any given context.

👍 10
Alex Miller (Clojure team)06:02:19

in both spec 1 and 2 you can create a spec object and pass it to any of the spec api functions

Alex Miller (Clojure team)06:02:41

with the caveat that s/keys relies on having registered key specs to rely upon

Alex Miller (Clojure team)06:02:50

there may be more support in spec 2 for map selection specs with anonymous key specs

Alex Miller (Clojure team)06:02:22

I am working in that area right now

caleb.macdonaldblack07:02:34

Ah okay i didnt know that

rascio14:02:37

Hi here! I'm playing with clojure specs, I'm struggling in trying to use generators for a spec with regex: (s/def ::entity (s/and string? #(re-matches #"DEST:\d+"))) Am I wrong in defining something? Or the regex can't be generated by the default lib?

borkdude14:02:41

@manuelrascioni you’re missing a %:

user=> (s/def ::entity (s/and string? #(re-matches #"DEST:\d+" %)))
:user/entity
user=> (s/valid? ::entity "DEST:1")
true

borkdude14:02:29

Your spec will generate, but the likelihood that it generates strings that will satisfy the predicate is extremely small. You probably want to provide a generator with the spec, using s/with-gen

rascio14:02:34

@borkdude ah...thank you! It seems I still have to train my eye to check for this kind of mistakes...I will check the docs for the with-gen thank you for the hint!

borkdude14:02:11

@manuelrascioni E.g.:

(s/def ::entity (s/with-gen (s/and string? #(re-matches #"DEST:\d+" %)) #(gen/fmap (fn [i] (str "DEST:" i)) (s/gen nat-int?))))
(gen/sample (s/gen ::entity))
("DEST:1" "DEST:0" "DEST:1" "DEST:2" "DEST:2" "DEST:7" "DEST:16" "DEST:41" "DEST:1" "DEST:4")

👍 5
rascio14:02:49

great! thank you, it is very helpful!

rascio14:02:27

just to check if I understood well, the fmap is used to "customize" the value generated by a generator, and return a generator, right?

borkdude14:02:46

fmap returns a new generator which transforms the values generated by the mapped-over generator using a function

Alex Miller (Clojure team)15:02:48

a newer better version of that is in test.chuck

borkdude15:02:23

I recently ran orchestra which tests ret-specs with speculative on a body of code. The only ret spec which wasn’t correct was for an fdef for which I had not used generative testing.

borkdude15:02:54

it was e.g. re-find, re-matches, etc. for which I had not considered that it could also return nils, as in

(re-find #"(a)?(b)" "b")
["b" nil "b"]
I wonder how I could have written a generator for this.

borkdude15:02:13

I would have to generate regexes and strings that would sometimes match, sometimes not

borkdude16:02:24

This fun experiment is able to generate regexes that seem to not terminate when executed…

(defn test-re-find []
  (let [regex-gen (gen/fmap (fn [parts]
                              (let [s (str/join parts)]
                                (re-pattern (str/join parts))))
                            (s/gen (s/* (s/cat :part
                                               (s/or :string string?
                                                     :group (s/with-gen string?
                                                              #(gen/fmap (fn [s]
                                                                           (str "(" s ")"))
                                                                         (s/gen string?))))
                                               :maybe (s/? (s/with-gen string?
                                                             #(gen/fmap (fn [s]
                                                                          (str s "?"))
                                                                        (s/gen string?))))))))
        matcher-gen (gen/fmap (fn [[r s]]
                                (re-matcher r s))
                              (gen/tuple regex-gen (s/gen string?)))]
    (map re-find (gen/sample matcher-gen 100))))

borkdude16:02:49

user=> (gen/sample regex-gen)
(#"" #"" #"" #"()15?" #"(2)" #"(3)" #"()9?F?JWwff1" #"xE7(re9)(79W)26E?()jKqXKk5?(DY1Xa)()(4m)2qNS3?" #"gW7GAJ9p22Z4o4eWJ?" #"(Gi8r)RQ22uBC?(jj0PolFmd)h7?Taz()(7)GwE0")

borkdude19:02:47

I’m trying this now:

(s/def ::regex.char #{"a" "b"})
  (s/def ::regex.group (s/with-gen string?
                         #(gen/fmap (fn [s]
                                      (str "(" s ")"))
                                    (s/gen ::regex.pattern))))
  (s/def ::regex.maybe (s/? (s/with-gen string?
                              #(gen/fmap (fn [s]
                                           (str s "?"))
                                         (s/gen ::regex.pattern)))))
  (s/def ::regex.pattern (s/* (s/or :char ::regex.char
                                    :group ::regex.group
                                    :maybe ::regex.maybe)))
This gives me a stackoverflow…
(binding [s/*recursion-limit* 1]
    (gen/sample (s/gen ::regex.pattern)))

borkdude20:02:20

How do I get generators to play nice with conformers?

(s/def ::my-spec (s/and int? (s/conformer str)))
(gen/sample (s/gen ::my-spec)) ;; => (0 -1 0 0 0 0 6 42 -1 -3) <- want strings here

seancorfield20:02:47

As written, your spec accepts (only) numbers -- and it is generating numbers that your spec accepts. That's the correct behavior.

seancorfield20:02:14

Conforming numbers to strings as part of a spec feels very wrong to me (and you know what an advocate I am for certain types of coercion in specs! 🙂)

🙂 5
borkdude20:02:23

this was just an example, not something I’m doing for real

borkdude20:02:36

the thing I was doing for real was the regex.pattern above, where I want to generate strings, but describe those strings in terms of spec

seancorfield20:02:38

Generators must produce values that are acceptable to your spec.

seancorfield20:02:23

(and we've had repeated cautions from @alexmiller not to use spec regex for string parsing/generation stuff 🙂 )

borkdude20:02:03

for parsing yes, because of performance, there are better tools, but for generation, I currently don’t know a better tool 😛

seancorfield20:02:07

Why not use test.chucks regex string generator?

seancorfield20:02:16

(or did I miss your rationale for not using that?)

borkdude20:02:41

I want to generate regexes, not strings that are matched by a given regex

borkdude20:02:19

once I have that, I can use test.chuck to generate strings from the generated regexes. and then I can use stest/check to test re-find, etc.

Alex Miller (Clojure team)20:02:11

why not make a regex for regexes?

aisamu22:02:20

Can't tell if serious

Alex Miller (Clojure team)23:02:08

I'm not sure either

😆 5
Alex Miller (Clojure team)20:02:19

then use test.chuck on it

borkdude20:02:04

regex language cannot be expressed with a regex, I think you need a CFG tool like spec

borkdude21:02:43

this kinda works:

(s/def ::regex.pattern
    (s/* (s/cat :pattern
                (s/alt :char #{\a \b}
                       :group (s/cat :open-paren #{\(}
                                     :inner-pattern ::regex.pattern
                                     :closing-paren #{\)}))
                :maybe (s/? #{\?}))))
  (s/valid? ::regex.pattern (seq "(ab)"))
  (s/valid? ::regex.pattern (seq "ab(ab)?"))
  (map str/join (binding [s/*recursion-limit* 2]
                  (gen/sample (s/gen ::regex.pattern))))
  
  (defn test-re-find []
    (let [regex-gen (gen/fmap (fn [r]
                                (re-pattern (str/join r)))
                              (s/gen ::regex.pattern))
          matcher-gen (gen/fmap (fn [[r strs]]
                                  (re-matcher r (str/join strs)))
                                (gen/tuple regex-gen (s/gen (s/* #{"a" "b"}))))]
      (let [matchers (gen/sample matcher-gen)]
        (map re-find matchers))))

  (test-re-find)
At least I’m now finding return values that I didn’t account for in an early version of the spec, e.g.:
["ba" "a" "a" "" "" nil nil "" "" ""]