I'm trying to use HSpec and QuickCheck to verify properties of Monoids (associativity and identity element). I am going to verify particular instances, but would like to keep most of the code polymorphic. This is what I came up with after several 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 though is polymorphic
monoidSpec = it "mappend mempty x = x" $ property prop_Monoid_mappend_mempty_x
and specify the actual Monoid instance (Sum, Product) and the type (Int, Double) later on. The issue is it wouldn't type check. 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 constraint Monoid in polymorphic version to be Arbitrary, Eq and Show but I don't know how.
The question is how to express specs for Monoid in a polymorphic way and avoid code duplication?
Notice the type of property :: Testable prop => prop -> Property
. The type var prop
is erased, and instance resolution can't take place if the type variable is no longer available. Basically what you want to do is defer instace selection, and to do that you must make the type available until the moment you pick the instance.
One way is to carry around an extra Proxy prop
parameter:
-- 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 library like tagged
which provides the type Tagged
, which simply adds some phantom type parameter to an 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.
Both of these require only -XScopedTypeVariables
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With