Making a Show with reflection
Show
is one of the most used type classes in Haskell1.
Being able to derive its instances is certainly convenient, but it becomes a
mess once we start playing with higher-kinded types, such as
Fix :: (Type -> Type) -> Type
.
Derived instance
newtype Fix f = Fix (f (Fix f))
deriving Show
The compiler complains:
<interactive>:4:39: error:
• No instance for (Show (f (Fix f)))
arising from the first field of ‘Fix’ (type ‘f (Fix f)’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Show (Fix f))
We can write (with sufficiently many extensions) the instance in the way suggested by GHC:
newtype Fix f = Fix (f (Fix f))
deriving instance Show (f (Fix f)) => Show (Fix f)
What is this constraint, Show (f (Fix f))
?
To show a value Fix x :: Fix f
, the derived instance needs to show
x :: f (Fix f)
, hence the constraint.
But that still looks bad. Essentially, that Show
instance
for Fix
is leaking an implementation detail, the
type to which f
is being applied.
A more modular solution would be a way to show
f a
parameterized by a way to show a
.
Something like forall a. Show a => Show (f a)
.
I’m looking forward to seeing Quantified class constraints to write that.
But what can we do in the meantime?
Handwritten instance
The currently standard solution uses a separate type class for
unary type constructors, Show1
. The constraint Show1 f
tells us that if we can show a
, then we can show f a
.
showsPrec1 :: (Show1 f, Show a) => Int -> f a -> ShowS
Unfortunately, GHC’s deriving mechanism doesn’t know how to use Show1
constraints, so we need to write the instance by hand to use
showsPrec1
instead of showsPrec
in the right places.
That’s exactly what the *recursion-schemes* library does
for Fix
.
This can be automated to some extent, but not entirely.
Show1
expresses the constraint forall a. Show a => Show (f a)
,
but sometimes the type parameter of f
is expected to be
phantom, so we would just require forall a. Show (f a)
.
It can be obtained as yet another type class, call it Show1'
, with the
overloaded helper:
showsPrec1' :: Show1' f => Int -> f a -> ShowS
We don’t want to write Show
instances by hand, but an automated
deriving mechanism has no way to know whether we want to use
Show1 f
or Show1' f
when it encounters a field of type f a
.
Thus, we need the ability to specify how each individual constructor field
should be shown, whether it is using Show
, Show1
, Show1'
, or yet
another method.
Looking back at GHC’s derived instance, it calls showsPrec
for every field;
that isn’t quite what we want. But showsPrec
is overloaded:
showsPrec :: Show a => Int -> a -> ShowS
Basically, Show a
can also be seen as a type of dictionaries,
and showsPrec
is a function of it.
showsPrec :: Show a -> Int -> a -> ShowS
Instead of taking the dictionary by explicit function application,
showsPrec
requires it implicitly by emitting a type class constraint.
Constraints are solved during type checking, and then overloaded
methods are desugared to regular functions.
It turns out that it is possible for us to catch those constraints and pass our own dictionaries. The magic can be found in the *reflection-extras* library, though its age makes it a bit clunky to use nowadays.
Semiautomatic deriving
For demonstration purposes, I whipped up a post-GHC-8-friendly prototype, with the following combinator (which can be generalized to other classes):
using :: forall a t. RShow a -> (Show a => t) -> t
The first parameter is a record RShow a
containing a method to be put in a
local Show
instance, used by the second parameter.
Via TH or Generics, we can obtain a generic implementation of showsPrec
,
which generates a Show (f (Fix f))
constraint, like the GHC-derived
instance does. Some TH scripts can be found in the *deriving-compat*
library. I know of no equivalent library using Generics.2
showsPrecFix :: Show (f (Fix f)) => Int -> Fix f -> ShowS
showsPrecFix = $(makeShow 'Fix) -- from deriving-compat
Then we can solve that constraint using Show1 f
and Show (Fix f)
.
instance Show1 f => Show (Fix f) where
showsPrec =
using
RShow { rshowsPrec = showsPrec1 @f @(Fix f) }
showsPrecFix
One inconvenient of this approach is that the *reflection* tricks behind the scenes rely on implementation details of type classes in GHC. It is unclear to me how stable these details can be assumed to be, although they seem to have been around for a good while.
Another issue is that reflection breaks the core principle of canonicity around which type classes are designed. Would a separate system of local instances make sense?
Citation needed.↩︎
In case you’re wondering whether *generic-deriving* does the job, it doesn’t derive
Show
, but a custom and entirely separateGShow
class.↩︎