How the Haskell Prelude avoids overlapping instances in Show
Last updated: Dec 11, 2022
In reading about overlapping instances in haskell, I came across this proposal that mentioned that:
[Overlapping types] can sometimes be simulated with the extra-method trick used in the Show class of the Prelude for showing lists of characters differently than lists of other things.
I’d never thought about how haskell-98 can support a show "asdf" that returns
"\"asdf\""
while show [1,2,3,4] returns
"[1,2,3,4]"
And I was surprised at the complexity of Show when I looked at the docs and
implementation (to be honest, I had no idea how the class was implemented or
that show was not the only method).
I thought the “extra-method trick” to avoid overlapping types was sufficiently cool and difficult to understand at first to warrant a short post about it. So here goes!
The basic problem
Remember that
type String = [Char]
so we can’t create an instance declaration for it in haskell-98. Something like
instance Show [Char] where
    ...
would require both FlexibleInstances and would also cause overlapping
instances when we declare
instance Show a=> [a] where
    ...
since a [Char] is also a Show a=> [a]. This will never do!
The solution
To get around this, the Creators added a showList method, which allows an
instance to define how a list of values of its type should be displayed. Here
is the Show class:
-- note: showS is just String -> String, a CPS optimization for building up strings
class Show a where
    showsPrec :: Int -> a -> ShowS
    show :: a -> String
    showList :: [a] -> ShowS
Here’s the instance declaration for [a] where I’ve added the default show
implementation from the class declaration:
instance Show a => Show [a]  where
    showsPrec _     = showList 
    show x          = shows x ""    -- default  
after a bit of indirection, we see show str is implemented as:
showList str "" 
where showList is from the Show instance for Char.
The “extra-method” trick pattern
So the general pattern here is when you have a perhaps-limited number of possibly-overlapping, recursive instances
instance Foo (Maybe Int) where
instance Foo (Maybe Bool) where
you can instead create an instance for the outer constructor:
instance Foo a=> Foo (Maybe a) where
define a new method (say maybeToFoo :: Maybe a -> Foo). And then delegate
the behavior of Maybe Int and Maybe Char to the Int and Char instances
respectively.
instance Foo Int where
instance Foo Bool where
Hopefully that makes sense.