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 tests
to 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
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")
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
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)
]
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
]