Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

0 votes
in test.check by
I think the syntax of {{clojure.core/for}} would be a good fit for test.check's combinators. For example:


(defn gen-even-subset
  "Returns a generator that generates an even-cardinality
   subset of the given elements"
  [elements]
  (gen/for [bools (apply gen/tuple (repeat (count elements) gen/boolean))
            :let [true-count (->> bools (filter identity) (count))]
            :when (even? true-count)]
    (->> (map list bools elements)
         (filter first)
         (map second)
         (set))))


This combines the capabilities of {{fmap}}, {{bind}}, and {{such-that}} into a familiar syntax.

One downside here is the temptation to use multiple clauses for independent generators, resulting in a use of {{gen/bind}} when {{gen/tuple}} would be simpler and presumably shrink easier. An approach to this is an additional supported clause, perhaps called {{:parallel}}, that uses the syntax of {{:let}} to provide the functionality of {{gen/tuple}}:


(gen/for [:parallel [n1 gen/nat
                     n2 gen/nat]
          :let [sum (+ n1 n2)]]
  {:nums [n1 n2] :sum sum})


Compared to {{gen/tuple}}, this has the advantage of placing generators syntactically next to names, rather than segregating the generators from the names.

The {{:parallel}} feature has not been added to the current patches.

10 Answers

0 votes
by
_Comment made by: gfredericks_

I think there might be some design ambiguity around the meaning of {{:when}}. In particular, in the following contrived example:


(for [n nat
      v (vec (return n))
      :let [sum (reduce + v)]
      :when (pos? sum)]
  v)


In my default design this can hang, for the same reason that this code can hang:


(bind nat
      (fn [n]
        (such-that
          (fn [v] (pos? (reduce + v)))
          (vector (return n)))))


But it could just as well have been written:


(such-that
  (fn [v] (pos? (reduce + v)))
  (bind nat (fn [n] (vector (return n)))))


So the issue is whether a {{:when}} filter is applied to just the previous generator or to all of the previous generators. I have some hazy notion that the latter would be less efficient in some cases, but I'm not sure what. So I think our options are:

# Decide to always do it one way or the other
# Provide a third keyword ({{:when-all}}?) with different behavior
# Don't write this macro at all because it's too difficult to understand

My gut is to do option 1 and just apply :when to the previous generator.
0 votes
by

Comment made by: gfredericks

Attached my initial draft. The implementation took a lot more thought than I expected, and is a bit subtle, so I included some inline comments explaining the structure of the macro.

0 votes
by

Comment made by: gfredericks

Attached TCHECK-15-p1.patch, updated to apply to the current master.

0 votes
by

Comment made by: gfredericks

Attached TCHECK-15-p2.patch which adds a note to the docstring about independent clauses, shrinking, and tuple.

0 votes
by

Comment made by: gfredericks

Attached TCHECK-15-p3.patch which fixes one bug and one redundancy in namespace aliases.

0 votes
by

Comment made by: gfredericks

Attached TCHECK-15-p4.patch which fixes a bug with destructuring (and adds a regression test).

0 votes
by

Comment made by: gfredericks

Also might be helpful to note that I've put this in my test.check utility library for now: https://github.com/fredericksgary/test.chuck#for.

0 votes
by

Comment made by: michaelblume

I wonder if it'd be possible to avoid :parallel by analyzing the code and checking whether the bindings can be run in parallel?

0 votes
by

Comment made by: gfredericks

I think it's possible in theory, but we'd need access to a non-buggy code walker.

Additionally you might argue that it makes the meaning of the code a lot more subtle.

0 votes
by
Reference: https://clojure.atlassian.net/browse/TCHECK-15 (reported by gfredericks)
...