Search code examples
haskellhunit

Pattern for generic unit test of type class instance implementations in Haskell


I was wondering if there was a known pattern for writing generic unit test code whose purpose it is to check (as a black box) the various instance (implementation of) a type class. For example:

import Test.HUnit

class M a where
foo :: a -> String
cons :: Int -> a     -- some constructor

data A = A Int
data B = B Int

instance M A where
  foo _ = "foo"
  cons  = A

instance M B where
  foo _ = "bar"     -- implementation error
  cons  = B 

I would like to write a function tests returning a Test with some way of specifying to tests the particular instance to which the code applies. I was thinking adding teststo the definition of the class with a default implementation (ignoring the coupling issue between testing code and actual code for now), but I can't simply have tests :: Test, and even if I try tests:: a -> Test (so having to artificially pass a concrete element of the given type to call the function), I cannot figure out how to refer to cons and foo inside the code (type annotations like (cons 0) :: a won't do).

Assuming I have class (Eq a) => M a where ... instead, with types A and B deriving Eq, I could trick the compiler with something like (added to the definition of M):

tests :: a -> Test
tests x = let 
            y = (cons 0)
            z = (x == y)       -- compiler now knows y :: a
          in
            TestCase (assertEqual "foo" (foo y)  "foo")

main = do
  runTestTT $ TestList
   [ tests (A 0)
   , tests (B 0)
   ]

But this is all very ugly to me. Any suggestion is warmly welcome


Solution

  • Proxy

    The current most common way of making a function polymorphic in an "internal" type is to pass a Proxy. Proxy has a single nullary constructor like (), but its type carries a phantom type. This avoids having to pass undefined or dummy values. Data.Proxy.asProxyTypeOf can then be used as an annotation.

    tests :: M a => Proxy a -> Test
    tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")
    

    proxy

    We can also generalize that type, as the Proxy is not actually being needed as a value. It's just a way of making a type variable non-ambiguous. You need to redefine asProxyTypeOf though. This is mostly a matter of style compared to the previous one. Being able to use more values as potential proxies can make some code more concise, sometimes at the cost of readability.

    -- proxy is a type variable of kind * -> *
    tests :: M a => proxy a -> Test
    tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")
      where
        asProxyTypeOf :: a -> proxy a -> a
        asProxyTypeOf = const
    

    Scoped type variables

    The function asProxyTypeOf, or your (==) trick are really a product of the inability to refer to a type variable from a signature. This is in fact allowed by the ScopedTypeVariables+RankNTypes extensions.

    Explicit quantification brings the variable a into scope in the body of the function.

    tests :: forall a proxy. M a => proxy a -> Test
    tests _ = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.
    

    Without the ScopedTypeVariables extension, cons 0 :: a would be interpreted as cons 0 :: forall a. a instead.

    Here's how you use these functions:

    main = runTestTT $ TestList
      [ tests (Proxy :: Proxy A)
      , tests (Proxy :: Proxy B)
      ]
    

    Type applications

    Since GHC 8, the AllowAmbiguousTypes+TypeApplications extensions make the Proxy argument unnecessary.

    tests :: forall a. M a => Test
    tests = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.
    
    main = runTestTT $ TestList
      [ tests @A
      , tests @B
      ]