Less boilerplate when defining generators
stevana opened this issue · comments
In Erlang QuickCheck generation is specified in two parts, first a weight function that assigns a frequency to a constructor given a model:
weight :: model Symbolic -> String -> Int
Erlang uses an atom for the constructor, above we simply use a string (perhaps this can be improved).
The second part is that we specify how to generate all arguments to an action. Let's introduce a concrete example:
data Action v :: * -> * where
Create :: Int -> Action v Ref
Use :: Reference v Ref -> String -> Action v ()
type Model v = [(Reference v Ref, String)]
Create
has an integer as argument and Use
has a Reference v Ref
and a string. So the specification would be something like:
genCreateArgs :: Model Symbolic -> Gen Int
genCreateArgs _ = arbitrary
genUseArgs :: Model Symbolic -> (Gen (Referenve v Ref), Gen String)
genUseArgs m = (elements (map fst m), arbtrary)
The two are then put together to create:
gen :: Model Symbolic -> Gen (Untyped Action)
gen m = frequency
[ (weight m "Create", Untyped <$> (Create <$> genCreateArgs m))
, (weight m "Use" , Untyped <$> (uncurry Use <$> genUseArgs m))
]
This last part can be template Haskelled away using the first two parts.
Thoughts?
Here's another idea.
User provides:
weight :: model Symbolic -> act Symbolic resp -> Int
and:
generators :: model Symbolic -> [Gen (Untyped act)]
In the library we can rescale the generators using the weight as follows (weight
and preconditions
should be parameters to these functions):
gweight :: model Symbolic -> Gen (Untyped act) -> Gen (Int, Gen (Untyped act))
gweight m gen = do
Untyped act <- gen
return (if preconditions m act then weight m act else 0, gen)
genHelper :: model Symbolic -> [Gen (Untyped act)]
-> Gen (Untyped act)
genHelper m gs = frequency =<< sequence (map (gweight m) gs)
So the old interface can be implemented as follows:
generator :: model Symbolic -> Gen (Untyped act)
generator m = genHelper m generators