使用HSpec和QuickCheck验证Data.Monoid属性

5

我想使用HSpec和QuickCheck来验证单子的属性(结合律和单位元素)。我将验证特定实例,但希望保持大部分代码是多态的。在经过几个小时的努力后,我得出了以下结论:

module Test where

import Test.Hspec
import Test.QuickCheck
import Data.Monoid

instance (Arbitrary a) => Arbitrary (Sum a) where
    arbitrary = fmap Sum arbitrary

instance (Arbitrary a) => Arbitrary (Product a) where
    arbitrary = fmap Product arbitrary

prop_Monoid_mappend_mempty_x x = mappend mempty x === x

sumMonoidSpec = it "mappend mempty x = x" $ property (prop_Monoid_mappend_mempty_x :: Sum Int -> Property)
productMonoidSpec = it "mappend mempty x = x" $ property (prop_Monoid_mappend_mempty_x :: Product Double -> Property)

main :: IO ()
main = hspec $ do
    describe "Data.Monoid.Sum" $ do
        sumMonoidSpec
    describe "Data.Monoid.Product" $ do
        productMonoidSpec

我希望你能够实现多态性,尽管这并不是必需的。
monoidSpec = it "mappend mempty x = x" $ property prop_Monoid_mappend_mempty_x

您可以稍后指定实际的 Monoid 实例 (Sum、Product) 和类型 (Int、Double)。问题在于它无法通过类型检查。我一直得到以下错误:

src/Test.hs@18:42-18:50 No instance for (Arbitrary a0) arising from a use of property
The type variable a0 is ambiguous
Note: there are several potential instances:
  instance Arbitrary a => Arbitrary (Product a)
    -- Defined at /home/app/isolation-runner-work/projects/68426/session.207/src/src/Test.hs:10:10
  instance Arbitrary a => Arbitrary (Sum a)
    -- Defined at /home/app/isolation-runner-work/projects/68426/session.207/src/src/Test.hs:7:10
  instance Arbitrary () -- Defined in Test.QuickCheck.Arbitrary
  ...plus 27 others …
src/Test.hs@18:51-18:79 No instance for (Monoid a0)
  arising from a use of prop_Monoid_mappend_mempty_x
The type variable a0 is ambiguous
Note: there are several potential instances:
  instance Monoid () -- Defined in Data.Monoid
  instance (Monoid a, Monoid b) => Monoid (a, b)
    -- Defined in Data.Monoid
  instance (Monoid a, Monoid b, Monoid c) => Monoid (a, b, c)
    -- Defined in Data.Monoid
  ...plus 18 others …

我知道我需要将Monoid约束在多态版本中为Arbitrary、Eq和Show,但我不知道如何实现。
问题是如何以多态方式表达Monoid的规范并避免代码重复?

你可能会对使用hspec-lawshspec-checkers感兴趣。 - Simon Hengel
1个回答

4
注意property :: Testable prop => prop -> Property中的类型。类型变量prop被擦除,如果类型变量不再可用,则无法进行实例解析。基本上,您想要做的是延迟实例选择,并且为此必须使类型在选择实例的时刻之前可用。
一种方法是携带额外的Proxy prop参数:
-- Possibly Uuseful helper function
propertyP :: Testable prop => Proxy prop -> prop -> Property 
propertyP _ = property 

monoidProp :: forall m . (Arbitrary m, Testable m, Show m, Monoid m, Eq m) 
           => Proxy m -> Property 
monoidProp _ = property (prop_Monoid_mappend_mempty_x :: m -> Property)

monoidSpec :: (Monoid m, Arbitrary m, Testable m, Show m, Eq m) => Proxy m -> Spec
monoidSpec x = it "mappend mempty x = x" $ monoidProp x 

main0 :: IO ()
main0 = hspec $ do
    describe "Data.Monoid.Sum" $ do
        monoidSpec (Proxy :: Proxy (Sum Int))
    describe "Data.Monoid.Product" $ do
        monoidSpec (Proxy :: Proxy (Product Double))

另一种方式是使用像tagged这样的库,它提供了类型Tagged,该类型只是向现有类型添加了一些幻影类型参数:

import Data.Tagged

type TaggedProp a = Tagged a Property 
type TaggedSpec a = Tagged a Spec 

monoidPropT :: forall a. (Monoid a, Arbitrary a, Show a, Eq a) 
            => TaggedProp a
monoidPropT = Tagged (property (prop_Monoid_mappend_mempty_x :: a -> Property))

monoidSpecT :: forall a . (Monoid a, Arbitrary a, Show a, Eq a) => TaggedSpec a
monoidSpecT = Tagged $ it "mappend mempty x = x" 
                          (unTagged (monoidPropT :: TaggedProp a))

main1 :: IO ()
main1 = hspec $ do
    describe "Data.Monoid.Sum" $ do
        untag (monoidSpecT :: TaggedSpec (Sum Int))
    describe "Data.Monoid.Product" $ do
        untag (monoidSpecT :: TaggedSpec (Product Double))

这些解决方案本质上是等价的,尽管在某些情况下一个可能更加便利。由于我不了解你的使用情况,所以我都包括了。

这两种解决方案只需 -XScopedTypeVariables


我稍微尝试了一下你的解决方案,(如果我错了请纠正我)看起来你实际上不需要propertyP,因为它无论如何都会忽略第一个参数,并且类型信息已经存在。在去掉propertyP之后,代理版本几乎与标记版本相同 - 只是调用站点使用代理时看起来更好一些。 - maciekszajna
严格来说,您不需要 propertyP,但它强制第一个和第二个参数具有相同的类型,这有时可以避免编写显式类型签名。 - user2407038

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接