无法为“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
将数字实例包装到新类型包装器中,即 Sum
和 Product
。
更进一步,你的陈述
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)
我尝试遵循 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
将数字实例包装到新类型包装器中,即 Sum
和 Product
。
更进一步,你的陈述
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)