test-check 2019-02-18

I wanted to generate arguments for subs:

(def subs-args-gen
  (gen/fmap
   (fn [[str [start end]]]
     (if end
       [str start end]
       [str start]))
   (gen/bind
    (gen/string)
    (fn [s]
      (gen/tuple (gen/return s)
                 (gen/bind
                  (gen/choose 0 (count s))
                  (fn [start]
                    (gen/one-of
                     [(gen/tuple (gen/return start))
                      (gen/tuple (gen/return start)
                                 (gen/choose start (count s)))]))))))))
(comment
  (gen/sample subs-args-gen)
;;  (["" 0] ["k" 1 1] ["" 0] ["…_" 0 0] ["" 0] ["¥/" 0] ["µ" 0] ["Þ¦\n" 3] ["" 0] ["\tsÈaíå" 1])
)
It feels a bit verbose, maybe there was a nicer way to do this?

I'm trying to think of something that shrinks better than all the gen/binds

and has a good distribution

those things can trade off with readability/simplicity sometimes

here's a relatively simple approach

(gen/let [[s start end use-end?]
          (gen/tuple gen/string gen/nat gen/nat gen/boolean)]
  (let [start (min start (count s))
        end (min end (count s))
        [start end] (sort [start end])]
    (cond-> [s start] use-end? (conj end))))

there might be edge cases there with (count s), I'm not sure how subs works

and you might complain that this will be more likely than you'd want to generate args that are equal to (count s), and if that bothers you then you could do something like use (gen/choose 0 1000000) for start and end and then scale them down to the size of the string

@gfredericks cool, I did not know about gen/let

the edge cases for subs I’ve already worked out, 0 <= start <= end <= (count s)

(yes even end equals to (count s) works)

So I got it right then I think

yeah, the distribution might be less nice, like you said

I like the boolean idea:

(gen/bind
   (gen/tuple (gen/string) (gen/boolean))
   (fn [[s end?]]
     (if end?
       (gen/bind
        (gen/choose 0 (count s))
        (fn [start]
          (gen/tuple
           (gen/return s)
           (gen/return start)
           (gen/choose start (count s)))))
       (gen/tuple
        (gen/return s)
        (gen/choose 0 (count s))))))

the sorting trick is also good for generating ordered numbers w/o having to do it in stages

I probably go overboard on avoiding gen/bind

you can actually write this fairly simply by using multiple clauses in gen/let

(which expands to gen/bind)

(gen/let [s gen-string, start (gen/choose 0 (count s)), end (gen/choose start (count s)), use-end? gen/boolean] (cond-> [s start] use-end? (conj end)))

if gen/tuple accepted nil, then you could write:

(fn [start]
          (gen/tuple
           (gen/return s)
           (gen/return start)
           (when end? (gen/choose start (count s)))))

I just have this instinct to not use gen/bind if it can be avoided, which is probably not worth the effort in a lot of cases

is gen/let a new thing? my REPL says it doesn’t exist

oh wait, gen from test.check, I think I’m using the one from spec

maybe new in 0.9.0?

yeah:

(g/let [s g/string
          start (gen/choose 0 (count s))
          end (gen/choose start (count s))
          use-end? (gen/boolean)]
    (cond-> [s start] use-end? (conj end)))
works. awesome.

so g/let is the monad-ish thing in test.check

yeah; it's just sugar over gen/fmap and gen/bind

totally useful sugar

this also works and avoids generating end if not needed:

(g/let [s g/string
          start (g/choose 0 (count s))
          use-end? g/boolean]
    (if use-end?
      (g/let [end (g/choose start (count s))]
        [s start end])
      [s start]))