无法为“Char”定义自定义“Arbitrary”实例,因为它已经存在

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

我尝试遵循 Introduction to Quickcheck 并想测试我的函数,该函数接受包含数字的字符串。为此,我为 Char:

定义了一个 Arbitrary 实例
instance Arbitrary Char where
    arbitrary = choose ('0', '9')

但是 ghc 抱怨说:

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’

如何告诉它忘记已经定义的实例并使用我自己的实例?或者它根本不会那样工作(这很奇怪,因为本教程采用了这种方法)?

正如@carsten-könig 所建议的,一个解决方案是为 Char 制作一个 newtype 包装器。这不是解决方法,而是一种正确且非常好的方法,可以避免与孤儿实例相关的整个 class 问题(在另一个模块中定义的 typeclasses 的实例),阅读更多关于此类问题here.

此外,当存在多个具有不同行为的可能实例时,这种方法被广泛使用。

例如,考虑在 Data.Monoid 中定义的 Monoid 类型 class 为:

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

正如您可能已经知道的那样,Monoid 是一种可以相互附加的值类型(使用 mappend),并且存在一个 'identity' 值 mempty 满足规则 mappend mempty a == a(将标识附加到值 a 会导致 a)。列表有一个明显的 Monoid 实例:

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

定义 Int 也很容易。确实整数加上加法运算形成一个正确的幺半群

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

但它是整数唯一可能的幺半群吗?当然不是,整数乘法会形成另一个适当的幺半群:

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

两个实例都是正确的,但现在我们遇到了一个问题:如果您尝试计算 1 `mappend` 2,则无法确定必须使用哪个实例。

这就是为什么 Data.Monoid 将数字实例包装到新类型包装器中,即 SumProduct

更进一步,你的陈述

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

可能会很混乱。它说 "I am an arbitrary character" 但只产生数字字符。在我看来,这会好得多:

newtype DigitChar = DigitChar Char deriving (Eq, Show)

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

小菜一碟。您可以更进一步,隐藏 DigitChar 构造函数,提供 digitChar 'smart constructor',这将不允许创建实际上不是数字的 DigitChar

关于你的问题"Do you know why that's not the approach the tutorial took?",我觉得原因很简单,教程好像是2006年写的,在those days quickcheck中根本就没有定义一个Arbitrary Char 的实例。所以教程中的代码在过去是完全有效的。

您不需要为临时测试输入生成创建新的 Arbitrary 实例。您可以使用 QuickCheck's forAll combinator 明确选择一个 Gen a 到一个函数:

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

prop_myFun = forAll digit $ \x -> isAwesome (myFun x)