3

I want to define a bunch of tests in a simple demo program, where each test is locally defined, but all can be printed in one standard place.

For example;

t1 = ("Sqrt(4)", sqrt(4.0))
...
t2 = ("sumList:", sum [1,2,3,4])
...
t3 = ("Description", value)
...

So each test is of type: (String, value), for various value types all of which (only) have to be members of the Show class.

Then for the summary of the tests, a loop:

test (msg, val) = do print $ msg ++ " :: " ++ show val
tests ts        = mapM test ts

This compiles, and assigns these types:

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.

So that they can vary on the actual types of the second argument, I tried something like this (pseudo code):

type ATest = (Show a) => (String, a)

since that wouldn't work, I tried:

{-# LANGUAGE RankNTypes #-}
type ATest = forall a. (Show a) => (String, a)

Which compiles, but still fails on any variation in the value argument.

Further, I want to abstract the type of tests from the loop which prints them, but I cannot then use it to convert from:

   test :: Show a => ([Char], a) -> IO ()
to
   test :: ATest -> IO ()

The basic idea was to define and use a polymorphic type for the tests in the definition of the testing loop. So perhaps a Data structure instead;

data (Show a) => ATest =  Test (String,a)

but that also fails, although it does give the right idea; all tests have a common structure, with a second value in the Show typeclass.

What is the right approach for this?

duplode
  • 33,731
  • 7
  • 79
  • 150
guthrie
  • 4,529
  • 4
  • 26
  • 31
  • I changed the title into something that reflects the topic a little more clearly, but I am not really satisfied with my suggestion. If anyone has better ideas, please do edit it. – duplode Nov 02 '16 at 04:56

2 Answers2

4

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.

Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150
  • Very nice, thank you. I do wonder why one needs to make the existential quantification (forall) explicit in the syntax for this and require a pragma. Why not just the same syntax as used for typeclasses, which was my original attempt? – guthrie Nov 06 '16 at 13:29
  • @guthrie Good question. I believe that feature is off by default merely because of simplicity, and also to shield beginners from strange collateral effects in case of typos (e.g. `data Foo = Bar | Baz a` when `Foo` was supposed to be a normal, non-existential parametric type). Cf. [the remarks in this Haskell Prime page](https://prime.haskell.org/wiki/ExistentialQuantification). By the way, `RankNTypes` (which adds uses of `forall` other than existential types) is also off by default mostly to keep things simple -- it introduces situations in which types can't be inferred without signatures. – duplode Nov 07 '16 at 01:47
3

There's no need to bend the type system in this case. You only need

t1 = ("Sqrt(4)", show $ sqrt(4.0))
...
t2 = ("sumList:", show $ sum [1,2,3,4])
...
t3 = ("Description", show $ value)
...

Since the only thing you can do with a result of a test is show it, you might as well do that right away. Thanks to laziness, no actual call to show is made until the result is needed.

Should you have a type class with many methods, an existential type might give you some marginal advantage.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • 1
    In general, this is good advice. I didn't follow this approach my answer (save for the allusions in the final paragraph) because I assumed the ultimate goal of the OP was writing `tests`, which is not an unreasonable thing to want to do in this specific context. – duplode Nov 02 '16 at 12:42
  • @duplode but he can write `tests` because `t1`, `t2` and `t3` are now all of the same type.That's kinda the point. – n. m. could be an AI Nov 02 '16 at 12:49
  • I see your point, and the difference of emphasis in your answer makes it a nice counterpoint to mine. If all the OP wants to do is to specify an *ad hoc* list of tests to be ran, though, there is a slight loss of convenience in having to define `t1`, `t2`, `t3`, etc. separately. While this sort of thing is almost always not enough to justify reaching for existential types, in the OP's specific use case the cost is small enough that it might make sense. – duplode Nov 02 '16 at 13:25
  • duplode: yes, that was my intent, and also to better understand how to do it this way. – guthrie Nov 06 '16 at 13:26