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)