Let's begin with your comment about the inferred types for test
and tests
:
test :: Show a => ([Char], a) -> IO ()
tests :: (Traversable t, Show a) => t ([Char], a) -> IO (t ())
Which works only as long as all tests have the same type for the
second argument. I assume that it is somehow specializing the type to
the actual encountered type of the arguments, even though they are all
show'able.
That is expected. All elements of a list (or any other traversable) must have the same type. It is not even quite a matter of "specializing": once type checker catches you trying to e.g. assemble a list out of an Int
and a String
, it immediately throws up its hands:
GHCi> [3 :: Int, "foo"]
<interactive>:125:12: error:
• Couldn't match expected type ‘Int’ with actual type ‘[Char]’
• In the expression: "foo"
In the expression: [3 :: Int, "foo"]
In an equation for ‘it’: it = [3 :: Int, "foo"]
A direct way of circumventing that is assigning the elements a type which ignores the irrelevant differences between the values. That is precisely what you were trying to do by bringing forall
into play -- in your case, you were trying to state that the only thing that matters about the second element of your pairs is that there is a Show
instance for them:
{-# LANGUAGE RankNTypes #-}
type ATest = forall a. (Show a) => (String, a)
You mention that this approach "still fails on any variation in the value argument". I couldn't reproduce this specific mode of failure: in fact, I couldn't even state the type of a list of ATest
:
GHCi> :set -XRankNTypes
GHCi> type ATest = forall a. (Show a) => (String, a)
GHCi> -- There is no special meaning to :{ and :}
GHCi> -- They are merely a GHCi trick for multi-line input.
GHCi> :{
GHCi| glub :: [ATest]
GHCi| glub = [("Sqrt(4)", sqrt(4.0)),("sumList:", sum [1,2,3,4])]
GHCi| :}
<interactive>:145:9: error:
• Illegal polymorphic type: ATest
GHC doesn't yet support impredicative polymorphism
• In the type signature:
glub :: [ATest]
GHC is forthcoming about its refusal: since ATest
is just a type synonym, [ATest]
expands to [forall a. (Show a) => (String, a)]
. The forall
within the argument to the list type constructor requires a feature called impredicative polymorphism, which is not supported by GHC. To avoid running into that, we need to define a proper datatype, and not just a synonym, which is what you tried to do in the final attempt -- except that you still need the forall
just like before:
GHCi> -- We need a different extension in this case.
GHCi> :set -XExistentialQuantification
GHCi> data ATest = forall a. Show a => ATest (String, a)
GHCi> :{
GHCi| glub :: [ATest]
GHCi| glub = [ATest ("Sqrt(4)", sqrt(4.0)),ATest ("sumList:", sum [1,2,3,4])]
GHCi| :}
GHCi>
This, at last, works as intended:
GHCi> :{
GHCi| -- I took the liberty of doing a few stylistic changes.
GHCi| test :: ATest -> IO ()
GHCi| test (ATest (msg, val)) = print $ msg ++ " :: " ++ show val
GHCi|
GHCi| tests :: Foldable t => t ATest -> IO ()
GHCi| tests = mapM_ test
GHCi| :}
GHCi> tests glub
"Sqrt(4) :: 2.0"
"sumList: :: 10"
On a general note, it is advisable to look for alternatives before committing yourself to use existential quantification in this manner, as it can often be more trouble than it is worth (for a primer on this discussion, see e.g. the question and all of the answers in How to convert my thoughts in OOP to Haskell?). In this case, however, in which all you want to do is to conveniently specify a list of tests to be ran, it seems sensible to use this strategy. In Testing QuickCheck properties against multiple types?, you can see an example of something very similar to what we have done here using QuickCheck, a full-blown testing library for Haskell.