Suppose I have a record type:
data Foo = Foo {x, y, z :: Integer}
A neat way of writing an Arbitrary instance uses Control.Applicative like this:
instance Arbitrary Foo where
arbitrary = Foo <$> arbitrary <*> arbitrary <*> arbitrary
shrink f = Foo <$> shrink (x f) <*> shrink (y f) <*> shrink (z f)
The list of shrinks for a Foo is thus the cartesian product of all the shrinks of its members.
But if one of these shrinks returns [ ] then there will be no shrinks for the Foo as a whole. So this doesn't work.
I could try saving it by including the original value in the shrink list:
shrink f = Foo <$> ((x f) : shrink (x f)) <*> ... {and so on}.
But now shrink (Foo 0 0 0) will return [Foo 0 0 0], which means that shrinking will never terminate. So that doesn't work either.
It looks like there should be something other than <*> being used here, but I can't see what.
Shrinking Shrinking is the mechanism by which a property-based testing framework can be told how to simplify failure cases enough to let it figure out exactly what the minimal reproducible case is. Sometimes the input required to find a failure can be fairly large or complex.
The shrinking uses the same runSub function (bound to the same seed) to search for a smaller counter example: This implementation makes use of the evalSubProp function, to get the (a -> Result) function required to explore the shrink tree: This implementation works in the sense that it will shrinking the counter example as we expect it would:
8. Shrinking Shrinking is the mechanism by which a property-based testing framework can be told how to simplify failure cases enough to let it figure out exactly what the minimal reproducible case is. Sometimes the input required to find a failure can be fairly large or complex.
These recursive applications of shrink build a tree, whose root is the initial value that led to a counter example. We will assume that this tree is built such that the children are ordered in such a way that it maximize the chances to find the smallest counter example.
If you want an applicative functor that will shrink in exactly one position, you might enjoy this one which I just created to scratch precisely that itch:
data ShrinkOne a = ShrinkOne a [a]
instance Functor ShrinkOne where
fmap f (ShrinkOne o s) = ShrinkOne (f o) (map f s)
instance Applicative ShrinkOne where
pure x = ShrinkOne x []
ShrinkOne f fs <*> ShrinkOne x xs = ShrinkOne (f x) (map ($x) fs ++ map f xs)
shrinkOne :: Arbitrary a => a -> ShrinkOne a
shrinkOne x = ShrinkOne x (shrink x)
unShrinkOne :: ShrinkOne t -> [t]
unShrinkOne (ShrinkOne _ xs) = xs
I am using it in code that looks like this, to shrink either in the left element of the tuple, or in one of the fields of the right element of the tuple:
shrink (tss,m) = unShrinkOne $
((,) <$> shrinkOne tss <*> traverse shrinkOne m)
Works great so far!
In fact, it works so well that I uploaded it as a hackage package.
I don't know what would be considered idiomatic, but if you want to ensure that every shrinking reduces at least one field without increasing the others,
shrink f = tail $ Foo <$> shrink' (x f) <*> shrink' (y f) <*> shrink' (z f)
where
shrink' a = a : shrink a
would do that. The Applicative
instance for lists is such that the original value is the first in the result list, so just dropping that gets you a list of values really shrunk, hence shrinking terminates.
If you want all fields shrunk if possible, and only unshrinkable fields to be retained as is, it is a bit more complicated, you need to communicate whether you have already gotten a successful shrink or not, and in case you haven't gotten any at the end, return an empty list. What fell off the top of my head is
data Fallback a
= Fallback a
| Many [a]
unFall :: Fallback a -> [a]
unFall (Fallback _) = []
unFall (Many xs) = xs
fall :: a -> [a] -> Fallback a
fall u [] = Fallback u
fall _ xs = Many xs
instance Functor Fallback where
fmap f (Fallback u) = Fallback (f u)
fmap f (Many xs) = Many (map f xs)
instance Applicative Fallback where
pure u = Many [u]
(Fallback f) <*> (Fallback u) = Fallback (f u)
(Fallback f) <*> (Many xs) = Many (map f xs)
(Many fs) <*> (Fallback u) = Many (map ($ u) fs)
(Many fs) <*> (Many xs) = Many (fs <*> xs)
instance Arbitrary Foo where
arbitrary = Foo <$> arbitrary <*> arbitrary <*> arbitrary
shrink f = unFall $ Foo <$> shrink' (x f) <*> shrink' (y f) <*> shrink' (z f)
where
shrink' a = fall a $ shrink a
maybe someone comes up with a nicer way to do that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With