3

Say I have a function:

safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead xs = Just $ head xs

And a test:

describe "Example.safeHead" $ do
  it "returns the head" $ do
    safeHead [1,2,3] `shouldBe` Just 1

  it "returns Nothing for an empty list" $
    safeHead [] `shouldBe` Nothing

This however produces:

No instance for (Eq a0) arising from a use of ‘shouldBe’
The type variable ‘a0’ is ambiguous
Note: there are several potential instances:
  instance (Eq a, Eq b) => Eq (Either a b)
    -- Defined in ‘Data.Either’
  instance forall (k :: BOX) (s :: k). Eq (Data.Proxy.Proxy s)
    -- Defined in ‘Data.Proxy’
  instance (GHC.Arr.Ix i, Eq e) => Eq (GHC.Arr.Array i e)
    -- Defined in ‘GHC.Arr’
  ...plus 88 others
In the second argument of ‘($)’, namely
  ‘safeHead [] `shouldBe` Nothing’
In a stmt of a 'do' block:
  it "returns Nothing for an empty list"
  $ safeHead [] `shouldBe` Nothing
In the second argument of ‘($)’, namely
  ‘do { it "returns the head"
        $ do { safeHead [...] `shouldBe` Just 1 };
        it "returns Nothing for an empty list"
        $ safeHead [] `shouldBe` Nothing }’

Why? And how can I fix it?

Abraham P
  • 15,029
  • 13
  • 58
  • 126
  • The type of `safeHead [] \`shouldBe\` Nothing` is ambiguous because there is nothing to tell the compiler which element type to use in comparisons of the list elements (of course, it does't matter, since there are no elements, but the compiler doesn't know that). You can fix it by giving an explicit type: `safeHead [] \`shouldBe\` (Nothing :: Maybe Int)`. – user2407038 Jan 16 '16 at 12:35

3 Answers3

4

As user2407038 commented, compiler doesn't know how to instantiate a. The fix he proposed is probably the best one -- you should specify type of a explicitly.

But for completeness I'd like to note, that there is other solution, extended default rules:

{-# LANGUAGE ExtendedDefaultRules #-}

describe "Example.safeHead" $ do
  it "returns the head" $ do
    safeHead [1,2,3] `shouldBe` Just 1

  it "returns Nothing for an empty list" $
    safeHead [] `shouldBe` Nothing

The extension modifies the standard defaulting rules to include more cases, e.g. Eq type class.

Added: After some thinking I reconsider my answer a bit. Results of unit testing of polymorphic functions should not depend of a particular way to instantiate type variables. So probably extended default rules is The Right Thing in tests? I never used them it real code, so I can't say for sure, but it definitely worth thinking about.

Yuras
  • 13,856
  • 1
  • 45
  • 58
  • very interesting. I'd love to know what the relative pros and cons of this solution are with respect to giving a type specification? Or is it literally "pros: less typing, cons: looser types"? – Abraham P Jan 16 '16 at 13:02
  • @AbrahamP basically yes, less typing is probably the only pros :) – Yuras Jan 16 '16 at 13:04
2
shouldBe                :: (Eq a, Show a) => a -> a -> Expectation
safeHead                ::         [a] ->    Maybe a
[]                      ::         [a]
safeHead []             ::                   Maybe a
Nothing                 ::                   Maybe a
shouldBe (safeHead [])  :: (Eq a, Show a) => Maybe a -> Expectation

shouldBe (safeHead []) Nothing ::                       Expectation -- but what's `a`?

As you can see, a is completely ambiguous. It can a be any type that has Show and Eq instances. This is also part of your error message:

No instance for (Eq a0) arising from a use of ‘shouldBe’
The type variable ‘a0’ is ambiguous

So pick one:

it "returns Nothing for an empty list" $
    safeHead [] `shouldBe` (Nothing :: Maybe ())

While you're at it, use QuickCheck to verify that safeHead works like head:

it "returns Just (head xs) for a non-empty list" $ property $ \(NonEmpty xs) ->
    safeHead xs `shouldBe` (Just (head xs) :: Maybe Integer)
Zeta
  • 103,620
  • 13
  • 194
  • 236
0

This code tests a Maybe Monad that returns a "Just" value:

     import Test.Tasty
     import Test.Tasty.HUnit
     import Data.List         

     main = defaultMain tests

     tests = testGroup "Tests uncons form Data.List"

     [testCase "Show uncons [1,2,3,4] = (1, [2,3,4])" $
     Just (1,[2,3,4]) @=? (uncons [1,2,3,4])]
hackbell
  • 31
  • 1
  • 6