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 Int
s. 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.
Char
into anewtype
likenewtype Digit = Digit Char
and make this into an instance – Dosage