Polymorphic pattern matching results in ambiguous type
It's instructive to think about how your code actually works at runtime. Type classes have dictionary passing semantics. Classy code
class Foo a where
foo :: a -> Int
instance Foo Int where
foo = id
useFoo :: Foo a => a -> IO ()
useFoo x = print (foo x)
callFoo = useFoo (123 :: Int)
is compiled into non-classy code.
data Foo_ a = Foo_ { foo :: a -> Int }
foo_Int :: Foo_ Int
foo_Int = Foo_ { foo = id }
useFoo :: Foo_ a -> a -> IO ()
useFoo dict x = print (foo dict x)
callFoo = useFoo foo_Int 123
The "fat arrow" =>
really means exactly the same thing at runtime as the "scrawny arrow" ->
. The only difference is that =>
's argument is passed implicitly by the constraint solver.
So let's think about your Show
example from this perspective. A type like test :: (Show a, Show b) => (a -> String, b -> String)
really means test :: (Show_ a, Show_ b) -> (a, String, b -> String)
. You can't factorise that function into a pair of functions (Show_ a -> a -> String, Show_ b -> b -> String)
. The body of the function might make use of both Show_
dictionaries before returning a result, so you have to supply both before you can get the (a -> String, b -> String)
out.
When inferring a type for resx
, the type checker will come up with resx :: (Show a, Show b) => a -> String
— it needs both instances in scope in order to call test
. Clearly this is an ambiguous type. b
doesn't appear to the right of the =>
, so at resx
's call site there will never be enough local type information to call it successfully.
You might object that one should be able to carry out such a factorisation in this case. The two halves of test
's body can't use each others' Show
instances, because Show
's methods only talk about their type parameter as an input and the input is not in scope in the wrong half of the tuple. But that's a sophisticated argument from parametricity about runtime behaviour, and it only holds for this specific case. It's not the sort of dumb syntax-directed reasoning a compiler is good at.
This makes sense: that pattern match doesn’t constrain a or b in any way, so they’re ambiguous. But how do I get rid of this error?
Giving type signatures will not help here. The type for test
is (Show a, Show b) => (a -> String, b -> String)
. Basically what you do is write:
resX :: Show a => a -> String
resX = fst test
The question is however, what should Haskell fill in for b
from the signature of test
? You might say that this does not matter, and here it indeed does not matter. But if you defined test
through a typeclass for example. The a
and b
type could determine together what the implementation for the first item would be. For example:
{-# LANGUAGE MultiParamTypeClasses #-}
class (Show a, Show b) => SomeTest a b where
test :: (a -> String, b -> String)
instance SomeTest Bool Bool where
test = (const "foo", const "bar")
instance SomeTest Bool Int where
test = (const "qux", const "bar")
here the second type parameter determines the implementation of the first item of test
, and hence we can not just omit that.
You can for example use the TypeApplications
extension, to provide a type for the other type parameter (here Bool
):
{-# LANGUAGE TypeApplications -#}
test :: (Show a, Show b) => (a -> String, b -> String)
test = (show, show)
resX :: Show a => a -> String
resX = fst (test @_ @Bool)
resY :: Show a => a -> String
resY = snd (test @Bool)