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

1 Answer 1

5

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
  ]
Sign up to request clarification or add additional context in comments.

1 Comment

This is great thank you!! Will try all these suggestions carefully as I expect to learn a lot from the exercise.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.