test.check

gen/for macro for alternate combinator syntax

Details

  • Type: Enhancement Enhancement
  • Status: Open Open
  • Priority: Minor Minor
  • Resolution: Unresolved
  • Affects Version/s: None
  • Fix Version/s: None
  • Component/s: None
  • Labels:
    None

Description

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.

  1. TCHECK-15.patch
    08/Apr/14 9:24 PM
    7 kB
    Gary Fredericks
  2. TCHECK-15-p1.patch
    13/Apr/14 8:33 PM
    6 kB
    Gary Fredericks
  3. TCHECK-15-p2.patch
    16/Apr/14 9:51 PM
    7 kB
    Gary Fredericks
  4. TCHECK-15-p3.patch
    16/Apr/14 9:58 PM
    7 kB
    Gary Fredericks
  5. TCHECK-15-p4.patch
    13/May/14 10:37 AM
    7 kB
    Gary Fredericks

Activity

Hide
Gary Fredericks added a comment -

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:

  1. Decide to always do it one way or the other
  2. Provide a third keyword (:when-all?) with different behavior
  3. 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.

Show
Gary Fredericks added a comment - 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:
  1. Decide to always do it one way or the other
  2. Provide a third keyword (:when-all?) with different behavior
  3. 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.
Hide
Gary Fredericks added a comment -

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.

Show
Gary Fredericks added a comment - 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.
Hide
Gary Fredericks added a comment - - edited

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

Show
Gary Fredericks added a comment - - edited Attached TCHECK-15-p1.patch, updated to apply to the current master.
Hide
Gary Fredericks added a comment -

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

Show
Gary Fredericks added a comment - Attached TCHECK-15-p2.patch which adds a note to the docstring about independent clauses, shrinking, and tuple.
Hide
Gary Fredericks added a comment -

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

Show
Gary Fredericks added a comment - Attached TCHECK-15-p3.patch which fixes one bug and one redundancy in namespace aliases.
Hide
Gary Fredericks added a comment -

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

Show
Gary Fredericks added a comment - Attached TCHECK-15-p4.patch which fixes a bug with destructuring (and adds a regression test).
Hide
Gary Fredericks added a comment -

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.

Show
Gary Fredericks added a comment - 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.

People

Vote (1)
Watch (1)

Dates

  • Created:
    Updated: