Use HSpec and QuickCheck to validate Data.Monoid properties

I am trying to use HSpec and QuickCheck to check the properties of monoids (associativity and identity element). I'm going to test specific instances, but would like to keep most of the code polymorphic. This is what I came up with after a few hours:

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

      

What I would like to have but is polymorphic

monoidSpec = it "mappend mempty x = x" $ property prop_Monoid_mappend_mempty_x

      

and provide the actual Monoid instance (Sum, Product) and type (Int, Double) later. The problem is it won't introduce validation. I keep getting

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 โ€ฆ

      

I know I need to constrain Monoid in polymorphic version to be arbitrary, Eq and Show, but I don't know how.

The question is how to express specifications for a monoid in a polymorphic way and avoid code duplication?

+3


source to share


1 answer


Pay attention to the type property :: Testable prop => prop -> Property

. The var type prop

is erased and instance resolution cannot take place if the type variable is no longer available. Basically what you want to do is defer selection, and for that you have to make the type available until you select the instance.

One way is to wrap an extra parameter 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))

      

Another way is to use a type library tagged

that provides a type tagged

that simply adds some type parameter of the phantom type to the existing type:



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))

      

These solutions are essentially equivalent, although in some cases one or the other may be much more convenient. Since I don't know enough about your use case, I've included both.

For both of them only is required -XScopedTypeVariables

.

+3


source







All Articles