Reputation: 28416
I have written a program in whcih I've defined several functions for which I've defined a concrete signature, say
work :: Foo -> [(Foo, Bar)] -> [(Foo, Bar)]
When approaching QuickCheck-based testing, the former signature is problematic, because it forces me to define Arbitrary
instance
s for Foo
, Bar
, and Co.
What to do? Going back to the code, I realize that work
and the other functions have in fact a polymorphic implementation, in the sense that I could relax the signature to a polymorphic one, provided a bunch of simple constraints, e.g.
work :: (Eq a, Ord b) => a -> [(a, b)] -> [(a, b)]
because Foo
and Bar
are smart wrappers to some numeric type like Int
, Word32
, and others.
But this is not the pill that it looks, because it turns out that with Foo
and Bar
I have enforced some runtime constraint on the values they wrap, e.g.
module Foo (Foo, makeFoo, int) where
newtype Foo = Foo { int :: Int }
makeFoo :: Int -> Foo
makeFoo 0 = error "Only positive numbers"
makeFoo i = Foo i
so relaxing the signature as I did above, and instantiating a test for, say, Int -> [(Int, String)] -> [(Int, String)]
, results in arbitrary
picking up 0
as a possible value, which work
doesn't expect because it was meant to work with Foo
, not Int
, even if it requires the same class
es from Foo
as it does from Int
.
What are the typical approaches to such a scenario?
Upvotes: 2
Views: 47
Reputation: 233150
I don't think one can give a general answer to a question like that, because there's likely to be a tipping point where, on one side of that tipping point, you could probably just get by with the built-in QuickCheck combinators and types, and on the other side, it's probably easier to define Arbitrary
instances for the types in question.
If you only need positive numbers, you can ask QuickCheck for Positive values. You can see examples of that in two of my articles:
On the other hand, it's usually not that hard to define Arbitrary
instances for your own custom types, again leveraging QuickCheck's functions. My article Naming newtypes for QuickCheck Arbitraries shows an example of that, too.
Haskell makes it easier than most other languages to capture custom rules as types instead of run-time checks, so if you can, I'd recommend reconsidering the design in order to make illegal states unrepresentable. On the other hand, I also understand that this isn't always practical, so I don't mean this advice in any absolute sense, and since I don't know the details of your code, I can't tell which way it falls.
Upvotes: 1