2

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

Sven Williamson
  • 1,094
  • 1
  • 10
  • 19

1 Answers1

4

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
  ]
Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56