Sometimes a function’s type class context grows so big that you would like to group the various type class constraints together and give it a sensible name, especially if that big context is used many times in your module. Consider for example the
fold function from module
fold :: (Fam phi, HFunctor phi (PF phi), Fold (PF phi)) => Algebra phi r -> phi ix -> ix -> r
Context synonyms aim to make it possible to give long contexts a name and reuse it throughout modules. Using a context synonym, the example above can be rewritten to:
context FoldFam phi = (Fam phi, HFunctor phi (PF phi), Fold (PF phi)) fold :: FoldFam phi => Algebra phi r -> phi ix -> ix -> r
Note that context synonyms are reminiscent of class aliases, but that proposal is a lot more involved than this one. Here we are only aiming to give long contexts a convenient short name.
During the fifth Haskell Hackathon, a team of programmers (including myself) set out to implement an extension to GHC to make exactly this possible. We were aware of a trick that makes context synonyms already somewhat possible: create a new class and make the right-hand side of the context synonym the superclass constraints of the new type class:
class (Fam phi, HFunctor phi (PF phi), Fold (PF phi)) => FoldFam phi
The problem with this approach, however, is that types for which you would like to use the
FoldFam constraint are not automatically instances of this new class. Only last week it dawned on me that you can remedy this by supplying one big general instance that makes every type an instance:
instance (Fam phi, HFunctor phi (PF phi), Fold (PF phi)) => FoldFam phi
Such an instance is sometimes used as in
instance Monad f => Applicative f where ..., but this doesn’t have the desired effect in Haskell. Our
FoldFam instance, however, is exactly what we want: match any type with that instance, then check the constraints. This also means we cannot add any more specific instances, which is also what we want.
So it seems we were trying to implement a feature that already sort of exists. To make things worse, Erik pointed out that this use of type classes and instances is documented literally in the GHC docs as old as version 6.0! If only we had known during the Hackathon.
“class (List t, List (ItemM t)) => Tree t”
This is indeed very nice. However I still think the context extension is worthy. The main advantage is to avoid UndecidableInstances. The good part is that internally the extension can be implemented using exactly this trick.
Yes, I failed to mention that you indeed need that extension.
Are you saying that ContextSynonyms should imply UndecidableInstances? I wonder how difficult it is to implement ContextSynonyms so that UndecidableInstances? is on only locally to the instance definition.
I would like not have to turn on UndecidableInstances.
However I’m wondering what happens if I import a module which use it. Do I fall into the undecidable world? In particular if a module use UndecidableInstances but only export decidable instances, is this module safe to import?
It requires UndecidableInstances.
Also, the “the instance is sometimes used” problem never materializes if you are careful to always define a new class and a sole fully universal instance for it.
I’ve used this fairly extensively for years, and I know it predates me in the Haskell community by a few years.
It doesn’t solve everything the class alias proposals attempt to solve though: You can’t retroactively define a superclass to an existing class for one; if you went to take Monad and rip it into a Bindable and Returnable class and glue that together with this style of alias, all the implementations of Monad would have to be refactored as well.
Leave a comment!
Martijn loves to receive comments! Add yours by filling out the fields below.