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?
source to share
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
.
source to share