Deriving Show for higher-kinded types
In a previous post,
I used reflection to resolve constraints from derived Show instances.
But this method is still quite cumbersome because every constraint must be handled
separately, and there can be as many of them as there are fields in our types.
Recently the problem of deriving Show was brought up on this month’s
Hask Anything thread on reddit,
and I found a better solution.
We want to derive Show for the following record type
(a functor functor):
data Bar f = Bar
{ bar1 :: f String
, bar2 :: f Int }If we try to derive it simply…
deriving instance Show (Bar f)… GHC first generates some code that looks roughly like this1…
instance Show (Bar f) where
show (Bar b1 b2) = "Bar (" ++ show b1 ++ ") (" ++ show b2 ++ ")"… in particular, it applies show to each field; this requires
the constraints Show (f String) and Show (f Int), and they
cannot be satisfied. Compilation fails.
We could add these constraints to the instance context. This works, but this solution doesn’t scale well to larger records with many types of fields.
deriving instance (Show (f String), Show (f Int)) => Show (Bar f)What we really mean here is that there is an instance
Show a => Show (f a), parametric in a, and indeed the
quantified constraints
proposal will soon solve this problem.
However, here is a solution without quantified constraints, that can be used with currently released versions of GHC.
The goal is to define an instance…
instance Show1 f => Show (Bar f) where
show = (???) -- To do… where Show1 is the closest approximation of the quantified constraint
forall a. Show a => Show (f a) we have at the moment.
A function show1 is available to render a value of type f a using a
Show1 f constraint2.
show1 :: (Show1 f, Show a) => a -> StringThe idea is that deriving produces the code we want for the most part, and
the result depends only on the “head” of the instance, Bar here, so that the
following declaration generates the same instance body (sketched above)
for any X and Y.
deriving instance X => Show (Bar Y)These unknowns X and Y may be different from Show1 f and f respectively;
we can still use that instance to then define the final instance
Show1 f => Show (Bar f) as follows…
instance Show1 f => Show (Bar f) where
show bar = show (convert bar)… under the following conditions:
the constraint
Xis entailed by the contextShow1 f; we might as well be as general as possible, soX = Show1 f;we can convert a
Bar fto aBar Y, i.e., there is a functionconvert :: Bar f -> Bar Y.
The function convert should not modify the information contained in the
record fields. This requirement is well met by
coerce.
It literally does not touch its argument; it is a function between types
that have the same runtime representation, and at runtime it behaves
like the identity function.
For Bar f to be coercible to Bar Y, the arguments f and Y must have
the same representation. We can’t modify the abstract f. Instead, we must
pick Y to be a newtype:
newtype Showing f a = Showing { unwrap :: f a }Looking at the derived instance, we thus have show (b1 :: Showing f String)
and show (b2 :: Showing f Int), and we expect the result to be equivalent to
show1 (unwrap b1 :: f String) and show1 (unwrap b2 :: f Int). This tells us
exactly how to implement Show for Showing.
instance (Show1 f, Show a) => Show (Showing f a) where
show = show1 . unwrapNote that this instance is not at all what the compiler would derive. In
particular, the Showing constructor is not rendered. The Showing type is not
as much a proper data type as it is a trick to transform a Show constraint
to a Show1 constraint.
We can now ask GHC to derive the following instance
(more about INCOHERENT below):
deriving instance {-# INCOHERENT #-} Show1 f => Show (Bar (Showing f))From that, we obtain the desired instance:
instance Show1 f => Show (Bar f) where
show = show . (coerce :: Bar f -> Bar (Showing f))And that’s it!
Here is a condensed version of the code in this post.
Little wrinkles
Just one function
The show . (coerce :: _) pattern with an explicit type annotation can be
condensed further into a single definition:
showing
:: forall h f
. (Coercible (h f) (h (Showing f)), Show (h (Showing f)))
=> h f -> String
showing = show . (coerce :: h f -> h (Showing f))We can now refactor the last instance:
instance Show1 f => Show (Bar f) where
show = showingOverlapping instances
The two instances Show (Bar (Showing f)) and Show (Bar f) overlap.
Without any annotation, that is not allowed.
Marking the former as OVERLAPPING helps (or equivalently, the latter as
OVERLAPPABLE), but then the latter instance will only be picked when f
is instantiated to not unify with the first. This means that functions that are
polymorphic in f may incur a constraint Show (Bar f), rather than the
simpler Show1 f. Instead, marking the former instance as INCOHERENT allows
the compiler to ignore the overlap and just pick whichever instance is most
convenient (usually the latter): Showing is only an internal type for the
purposes of deriving Show, and shouldn’t be used elsewhere, so it is okay to
assume that f does not ever unify with Showing g.
The derived code actually defines
showsPrecfirst to deal with precedence levels, and thenshowjust wraps it.↩︎Actually there is
showsPrec1to replace/implementshowsPrec.↩︎