Overview
The list of type class constraints in a function signature can
sometimes get out of hand. In these situations, we can introduce a
type synonym (thanks to ConstraintKinds
) to avoid repetition.
Say we want to group together the Show
and Read
constraints:
Now Serialise a
can be used anywhere where we require both constraints:
This is great, because it means we no longer have to spell out (Show a, Read a)
whenever we need both, and we also improved readability, because
Serialise
conveys some additional domain-specific meaning.
There’s a problem with this, however. If we ask GHCi about the type of
roundtrip
:
it will eagerly expand the type synonym, removing all traces of
Serialise
. Of course this is a well known problem of type
synonyms, so we generally avoid them in favour of newtype
s.
But there’s no analogous construction for constraints. Or is there?
Constraints newtypes (kind of)
To begin, we’re going to drop the type synonym in favour of the “constraint synonym” technique, which is essentially the following:
In other words, we introduce a new type class with the required superclass constraints, and a single catchall instance.
So far, the status quo hasn’t improved though. GHC is quite renitent:
This happens because the compiler sees that there’s only one matching instance, so it’s safe to pick that one, and it will do so. This point is the important one: that there’s only one instance. So, if we could somehow trick GHC into thinking that there are other options, then maybe it wouldn’t be so eager to expand our constraints.
So, we create an empty data type, only to be used internally:
Next, we satisfy the superclass constraints
Note that these two instances only exist so that the constraint is satisfied, but since the type is internal, the actual functions are never going to be invoked.
Finally, the key ingredient: an overlapping instance for Serialise Opaque
.
Now, every time GHC sees a Serialise a
constraint, it will no longer
be able to pick the catchall instance, in case a
gets instantiated
to Opaque
later. Of course, this won’t happen, because we don’t
export Opaque
, but it’s good enough for GHC.
A real world example
You might say that the (Show a, Read a)
example is perhaps overly
simplistic. I came up with this technique to solve a very real problem
in the generic-lens library.
This problem shows up at many places in the library, but to pick one, consider the AsType
class:
The exact meaning of the class is irrelevant here (but see the
documentation if you’re interested). What
matters is that there’s a catchall instance defined for all types
(using GHC.Generics
), which in turn requires a large number of other constraints and predicates
to hold. Since this catchall instance is the only one defined by the library, asking for the
type of _Typed
in GHCi eagerly expands the constraints to those of the instance.
Not great. All the internal implementation details leak out. By
employing the opaque constraint trick above, we can define overlapping
instances for the AsType
class, which results in the following type signature:
which is much nicer!
Acknowledgements
I wrote most of this post a while time ago, but never published it. Thanks to Rob Rix for bringing up this topic and thus reminding me to publish it. It’s good to see library authors care about the user experience of their library down to this level of detail, and I hope this technique will be useful for many others!