2
votes

I'm using hspec and QuickCheck to validate functor laws for instances of Functor. I have functions

functorIdentity :: (Functor f, Eq (f a)) => f a -> Bool

and

functorComposition :: (Functor f, Eq (f c)) => (Fun a b) -> (Fun b c) -> f a -> Bool

I'm then testing these two using blocks of code like this:

testListFunctorness :: IO ()
testListFunctorness =
  hspec $ do
    describe "list" $ do
      it "should obey functor identity" $ do
        property (functorIdentity :: [Int] -> Bool)
      it "should obey functor composition" $ do
        property
          (functorComposition :: (Fun Int String) -> (Fun String Int) -> [Int] -> Bool)

The thing is, to test the same properties for a different Functor instance, I need to copy everything except the [Int]s:

testMaybeFunctorness :: IO ()
testMaybeFunctorness =
  hspec $ do
    describe "maybe" $ do
      it "should obey functor identity" $ do
        property (functorIdentity :: Maybe Int -> Bool)
      it "should obey functor composition" $ do
        property
          (functorComposition :: (Fun Int String) -> (Fun String Int) -> Maybe Int -> Bool)

It feels like I should be able to write an expression which is in some way polymorphic over different Functor instances, but I can't even think how to begin that.

How can I conveniently reuse that block of test logic for multiple different Functors?

1
I'm 99% sure I have an AB problem here but I can't see what the B is :)N3dst4

1 Answers

3
votes

What you could do is to explicitly pass the desired type to testFunctorness:

import Data.Proxy

testFunctorness :: forall a. Functor a => Proxy a -> IO ()
testFunctorness _ =
  hspec $ do
    describe "the type" $ do
      it "should obey functor identity" $ do
        property (functorIdentity :: a Int -> Bool)
      it "should obey functor composition" $ do
        property
          (functorComposition :: (Fun Int String) -> (Fun String Int) -> a Int -> Bool)

The call would look like testFunctorness (Proxy :: Proxy []).

You will need to enable {-# LANGUAGE ScopedTypeVariables #-} so that the a inside the function refers to the a in the type signature. The forall is then needed to let the typechecker know this a should be lexically scoped.