Enlico
Enlico

Reputation: 28416

How to approach testing for polymorphic functions taking newtype smart wrappers around simple types?

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 instances 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 classes from Foo as it does from Int.

What are the typical approaches to such a scenario?

Upvotes: 2

Views: 47

Answers (1)

Mark Seemann
Mark Seemann

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

Related Questions