Can't define custom `Arbitrary` instance for `Char` since it already exists
Asked Answered
S

2

6

I tried following the Introduction to Quickcheck and wanted to test my function which takes strings containing of digits. For that, I defined an Arbitrary instance for Char:

instance Arbitrary Char where
    arbitrary = choose ('0', '9')

But ghc complains about that:

A.hs:16:10:
Duplicate instance declarations:
  instance Arbitrary Char -- Defined at A.hs:16:10
  instance [overlap ok] [safe] Arbitrary Char
    -- Defined in ‘Test.QuickCheck.Arbitrary’

How can I tell it to forget about the already defined instance and use my own instance? Or will it not work that way at all (which would be strange, since the tutorial takes that approach)?

Salvage answered 11/4, 2015 at 12:25 Comment(2)
I would just wrap Char into a newtype like newtype Digit = Digit Char and make this into an instanceDosage
@CarstenKönig, very good workaround. Also prevents me from using general chars where I should only be using digits. Do you know why that's not the approach the tutorial took?Salvage
D
11

As @carsten-könig adviced, a solution would be to make a newtype wrapper for Char. This is not a workaround, but a proper and really nice way to escape a whole class of problems that are related to orphan instances (instances for typeclasses which are defined in another module), read more about such problems here.

Moreover, this approach is widely used when there are several possible instances with different behaviour.

For example, consider the Monoid typeclass which is defined in Data.Monoid as:

class Monoid a where
    mempty  :: a           -- ^ Identity of 'mappend'
    mappend :: a -> a -> a -- ^ An associative operation

As you may already know Monoid is a type of values which can be appended to each other (using mappend) and for which there exists an 'identity' value mempty which satisfies a rule mappend mempty a == a (appending an identity to value a results in a). There is an obvious instance of Monoid for Lists:

class Monoid [a] where
    mempty  = []
    mappend = (++)

It's also easy to define Ints. Indeed integers with addition operation form a correct monoid.

class Monoid Int where
    mempty  = 0
    mappend = (+)

But is it the only possible monoid for integers? Of course it isn't, multiplication on integers would form another proper monoid:

class Monoid Int where
    mempty  = 1
    mappend = (*)

Both instances are correct, but now we have a problem: if you would try to evaluate 1 `mappend` 2, there is no way to figure out which instance must be used.

That's why Data.Monoid wraps the instances for numbers into newtype wrappers, namely Sum and Product.

Going further, your statement

instance Arbitrary Char where
    arbitrary = choose ('0', '9')

might be very confusing. It says "I am an arbitrary character" but produces only digit characters. In my opinion this would be much better:

newtype DigitChar = DigitChar Char deriving (Eq, Show)

instance Arbitrary DigitChar where
    arbitrary = fmap DigitChar (choose ('0', '9'))

A piece of cake. You can go further and hide a DigitChar constructor, providing digitChar 'smart constructor', which wouldn't allow to create a DigitChar which is not actually a digit.

As of your question "Do you know why that's not the approach the tutorial took?", I think the reason is simple, the tutorial seems to be written in 2006, and in those days quickcheck simply didn't define an Arbitrary instance for Char. So the code in tutorial was perfectly valid back in the days.

Dennisedennison answered 11/4, 2015 at 13:52 Comment(3)
@raxacoricofallapatorius Could you give some details on what doesn't work?Dennisedennison
I get: Couldn't match expected type ‘Gen DigitChar’ with actual type ‘DigitChar’ In the expression: DigitChar $ choose ('0', '9')Vesicle
@raxacoricofallapatorius Oh, seems like I have a small mistake in that sniplet. You actually need to use fmap in order to apply DigitChar I've updated the sniplet.Dennisedennison
G
0

You do not need to create new Arbitrary instances for ad-hoc test input generation. You can use QuickCheck's forAll combinator to explicitly pick a Gen a to a function:

digit :: Gen Char
digit = choose ('0', '9)

prop_myFun = forAll digit $ \x -> isAwesome (myFun x)
Goldshell answered 31/1, 2016 at 11:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.