packages feed

quickcheck-helpers-1.0.0: README.md

# `quickcheck-helpers`

## What is this?

Consider a typical product type:

```haskell
data Pair a = Pair a a
```

To use `Pair` in QuickCheck tests, you need an `Arbitrary` instance. For product
types, the `arbitrary` method is easy to define:

```haskell
instance Arbitrary a => Arbitrary (Pair a) where
    arbitrary = Pair <$> arbitrary <*> arbitrary
```

However, if we try to define `shrink` similarly, we will get serious issues:

```haskell
instance Arbitrary a => Arbitrary (Pair a) where
    -- Fine
    arbitrary = Pair <$> arbitrary <*> arbitrary
    -- Very not fine
    shrink (Pair x y) = Pair <$> shrink x <*> shrink y
```

Why? Consider `Pair 0 6 :: Pair Int`. We expect that this should still shrink:
even though the first field is not shrinkable, the second one still is. If we
try to shrink, however, we get an unpleasant surprise:

```
>>> shrink (Pair 0 6 :: Pair Int)
[]
```

In general, if we define shrinkers for product types in the way shown above,
shrinking will stop when _any_ field is non-shrinkable. This is not a
consequence of the syntax per se: even if we rewrite it monadically, this still
happens.

To avoid this, we have two options. One is to manually write the shrinker in a
very specific way:

```haskell
instance Arbitrary a => Arbitrary (Pair a) where
    -- No change
    arbitrary = Pair <$> arbitrary <*> arbitrary
    -- Fix, but at what cost?
    shrink (Pair x y) = (Pair <$> pure x <*> shrink y) <|>
                        (Pair <$> shrink x <*> pure y)
```

This will solve the problem, but it's tedious, error-prone and very long.

Another option is to derive `Generic` for your type, then use `genericShrink`
from QuickCheck:

```haskell
data Pair a = Pair a a
    deriving stock (Generic)

instance Arbitrary a => Arbitrary (Pair a) where
    -- No change
    arbitrary = Pair <$> arbitrary <*> arbitrary
    shrink = genericShrink
```

This will also solve the problem, but comes at significant costs in compile-time
performance, run-time performance and a complete and utter lack of clarity about
what actual code gets generated. These are all problems typical of `Generic`
use. However, there is also a deeper problem with this solution. Consider the
following data type:

```haskell
data CountedList a = CountedList Int [a]
    deriving stock (Generic)
```

This data type requires maintaining a _non-structural_ invariant about the `Int`
field. Specifically, the `Int` field must be exactly the length of the `[a]`
field. However, if we attempt to use `genericShrink` with this type:

```haskell
instance Arbitrary a => Arbitrary (CountedList a) where
    arbitrary = do
        NonNegative len <- arbitrary
        CountedList len <$> vectorOf len
    shrink = genericShrink
```

you will end up with many nonsensical shrinks:

```
shrink (CountedList 2 [1, 3] :: CountedList Int)
>>> [CountedList 0 [1,3],CountedList 1 [1,3],CountedList 2 [],CountedList 2 [3],CountedList 2 [1],CountedList 2 [0,3],CountedList 2 [1,0],CountedList 2 [1,2]]
```

Thus, both solutions are in some way inadequate.

`quickcheck-helpers` solves this problem, allowing both concise definitions of
`shrink` for product types (mirroring `arbitrary`) while also giving full
control if necessary.

## How do I use it?

For the `Pair` example above:

```
import Test.QuickCheck.Helpers (toShrink, neShrink)

data Pair a = Pair a a

instance Arbitrary a => Arbitrary (Pair a) where
    -- No change
    arbitrary = Pair <$> arbitrary <*> arbitrary
    -- Using the new functionality
    shrink (Pair x y) = toShrink (Pair <$> neShrink x <*> neShrink y)
```

This solution has both the conciseness of the original (and wrong) shrinkers,
while also giving the improved shrinks of either of the naive solutions, without
the downsides.

## What can I do with this?

This library is licensed under the MIT license. See the `LICENSE.md` file for
details.