quickcheck icon indicating copy to clipboard operation
quickcheck copied to clipboard

RFC: Create a SuchThat data type that allows call site `suchThat`

Open eborden opened this issue 7 years ago • 8 comments

I was recently writing tests where I needed some properties to hold in a specific situation. My usual path here is to make a newtype to get the Arbitrary instance I want. However I realized that suchThat could be used at the call site via Testable. What do you think of this type?

data SuchThat a prop = SuchThat (a -> Bool) (a -> prop)

instance (Arbitrary a, Show a, Testable prop) => Testable (SuchThat a prop) where
  property (SuchThat predicate prop) =
    forAllShrink (arbitrary `suchThat` predicate) shrink prop

This allows Arbitrary instances to be decorated via a generic combinator at the call site.

Here is a trivial example that would always pass:

it "is greater than 0" . property . SuchThat (> 0) $ \int -> int > 0

eborden avatar Apr 24 '18 17:04 eborden

Nice one!

Lysxia avatar Apr 24 '18 20:04 Lysxia

I like it!

One comment: it could instead be a property combinator in the same style as e.g. forAll:

??? :: (Arbitrary a, Testable prop) => (a -> Bool) -> (a -> prop) -> Property

However, the name suchThat is already taken, and I'm not sure what a good available name would be.

Another comment: it should also check the predicate during shrinking (i.e. shrink should be filter predicate . shrink).

nick8325 avatar Jun 24 '18 20:06 nick8325

forAllSuchThat, forAllSuchThatShrink, forAllSuchThatShrinkShow? Those are mouthful, but are consistent

phadej avatar Jun 24 '18 20:06 phadej

I like it! If we had this, would we still have a need for (==>)? Not suggesting removal, but maybe favoring SuchThat/forAllSuchThat/... in docs?

tom-bop avatar Jun 25 '18 17:06 tom-bop

@tom-bop it's matter of taste which is nicer:

it "is greater than 0" . property . SuchThat (> 0) $ \int -> int > 0
it "is greater than 0" . property . $ \int -> int > 0 ==> int > 0

phadej avatar Jun 26 '18 00:06 phadej

I think the disadvantage of (==>) is that it discards.

E.g. if this runs 100 tests:

quickCheck $ SuchThat even $ \i -> ...

This will only run (i.e. not skip) ~50:

quickCheck $ \i -> even i ==> ...

tom-bop avatar Jun 26 '18 15:06 tom-bop

Would this be the recommended way to handle more than one test argument?:

quickCheck $ SuchThat (\(i0, i1) -> even i0 && odd i1) $ \(i0, i1) -> i0 /= i1

tom-bop avatar Jun 26 '18 15:06 tom-bop

I think the disadvantage of (==>) is that it discards.

E.g. if this runs 100 tests:

quickCheck $ SuchThat even $ \i -> ...

This will only run (i.e. not skip) ~50:

quickCheck $ \i -> even i ==> ...

No, QuickCheck continues to run tests until it reaches (by default) 100 non-skipped tests.

The difference between ==> and suchThat is when you have constraints on multiple arguments:

  1. \x y -> p x && q y ==> ...

  2. SuchThat p $\x -> SuchThat q $ \y -> ...

(1) will generate a whole test case (x and y), then throw it out if the precondition does not hold. (2) will generate x and y independently (when generating y, if q y does not hold it will not retry generating x). (2) will be quicker to succeed if there are lots of independent conditions on different test arguments (since (1) has to satisfy them all simultaneously), but it also has the risk of hanging if it makes a choice of x which makes it impossible to satisfy q y.

nick8325 avatar Jun 28 '18 12:06 nick8325