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
Proxy
The current most common way of making a function polymorphic in an "internal" type is to pass a
Proxy.Proxyhas a single nullary constructor like(), but its type carries a phantom type. This avoids having to passundefinedor dummy values.Data.Proxy.asProxyTypeOfcan then be used as an annotation.proxy
We can also generalize that type, as the
Proxyis not actually being needed as a value. It's just a way of making a type variable non-ambiguous. You need to redefineasProxyTypeOfthough. 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.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 theScopedTypeVariables+RankNTypesextensions.Explicit quantification brings the variable
ainto scope in the body of the function.Without the
ScopedTypeVariablesextension,cons 0 :: awould be interpreted ascons 0 :: forall a. ainstead.Here's how you use these functions:
Type applications
Since GHC 8, the
AllowAmbiguousTypes+TypeApplicationsextensions make theProxyargument unnecessary.