How QuickCheck generates random functions
TweetIn QuickCheck, test data is produced by test generators whose types are of the form Gen a
. Gen a
is a generator for values of type a
. A type can define a default test data generator by defining an instance of the class Arbitrary
:
class Arbitrary a where
arbitrary :: Gen a
...
We can define instances of Arbitrary
using the combinators provided by QuickCheck. For example, the instances of Bool
and Ordering
are defined using choose
and elements
respectively. choose
generates a random element in the given inclusive range and elements xs
generates an arbitrary element of the list xs
.
instance Arbitrary Bool where
arbitrary = choose (False,True)
shrink True = [False]
shrink False = []
instance Arbitrary Ordering where
arbitrary = elements [LT, EQ, GT]
shrink GT = [EQ, LT]
shrink LT = [EQ]
shrink EQ = []
Simple and easy! But how about a function? Can we generate a function in the same way? The answer is surprisingly yes, but you need another type class named CoArbitrary
.
Before explaining how CoArbitrary
works, we need to check how Gen
is defined:
-- | A generator for values of type @a@.
newtype Gen a = MkGen{
unGen :: QCGen -> Int -> a -- ^ Run the generator on a particular seed.
-- If you just want to get a random value out, consider using 'generate'.
}
Internally, Gen a
is a function which takes 2 arguments (QCGen
and Int
) and returns a
. Here QCGen
is a newtype wrapper around either StdGen
or TFGen
.
So Gen (a -> b)
expands to QCGen -> Int -> a -> b
. By reordering parameters, this is equivalent to a -> Int -> QCGen -> b
, which represents a -> Gen b
. Thus by defining
promote :: (a -> Gen b) -> Gen (a->b)
we can produce a generator for a function type, provided that we can construct a generator for the result type which somehow depends on the argument value.
So we need coarbitrary
which modifies a generator in a way depending on its first parameter.
class CoArbitrary a where
coarbitrary :: a -> Gen b -> Gen b
To actually define an instance of CoArbitrary
, we need a helper function variant
, which perturbs the generator. It creates a new generator that produces different pseudo-random results than the original.
-- | Modifies a generator using an integer seed.
variant :: Int -> Gen a -> Gen a
Now we can define instances of CoArbitrary
.
instance CoArbitrary Bool where
coarbitrary False = variant 0
coarbitrary True = variant 1
instance CoArbitrary a => CoArbitrary (Maybe a) where
coarbitrary Nothing = variant 0
coarbitrary (Just x) = variant 1 . coarbitrary x
With all the pieces in place, we can finally define an Arbitrary
for the function type a -> b
:
instance (Coarbitrary a, Arbitrary b) => Arbitrary (a -> b) where
arbitrary = promote (\a -> coarbitrary a arbitrary)
To see how this works:
\a -> coarbitrary a arbitrary
has typea -> Gen b
promote
has type(a -> Gen b) -> Gen (a->b)
- So, the entire expression has type
Gen (a->b)
The current implementation of QuickCheck is a bit different as it is generalized to Monad
, but When m
is the function instance of Monad
, promote
is the same as we derived here.
promote :: Monad m => m (Gen a) -> Gen (m a)
References
- QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs by Koen Claessen and John Hughes
- StackOverflow: How to generate random, typed functions