How to define overlappable depending instances?

166 views Asked by At

So I have a typeclass somewhat like this

class Stringify x where
  stringify :: x -> String

and I have two other typeclasses somewhat like those

class LTextify x where
  ltextify :: x -> L.Text
class Textify x where
  textify :: x -> T.Text

and I want to define dependent typeclasses

class (LTextify x) => Stringify x where
  stringify = L.unpack . ltextify
class (Textify x) => Stringify x where
  stringify = T.unpack . textify

But of course haskell complains about "duplicate instance declarations" which of course is true. I've tried playing around with OVERLAPPABLE and OVERLAPPING, but to no avail. What I want is, that if a class has an instance of LTextify, then that should be used. If not, then if it has an instance of Textify than that should be used to implement Stringify. And I want a class to also be able to explicitely define an instance for Stringify which then overlaps a possibly existing instance of Textify or LTextify. Of course in a deterministic way.

1

There are 1 answers

0
Hjulle On BEST ANSWER

I'm assuming those last two should be instance declarations and not class declarations?

instance (LTextify x) => Stringify x where ...
instance (Textify x) => Stringify x where ...

Having different behaviour depending on if an instance exists or not really supported in Haskell, since orphan instances means that the list of available instances can change in unpredictable ways depending on imports. I recommend (if possible) to instead make Stringify a superclass of both Textify and LTextify.

Additionally, these particular instances would be impossible to make, because the first step of GHC's instance resolution is to discard all constraints and check which instance would match structurally, so it would look like this:

instance Stringify x
instance Stringify x

There is no way to distinguish between these two at all, so ghc would have to arbitrary chose between one of them and hope it's the right one.

In order to use the OVERLAPPING pragmas, there needs to be one instance that is strictly more specific than the other, like

instance C a => Foo [a]
instance {-# OVERLAPPING #-} Foo [Char]

See this section in the user manual for a more in depth explanation of how it works. Here's a relevant quote:

GHC requires that it be unambiguous which instance declaration should be used to resolve a type-class constraint. GHC also provides a way to loosen the instance resolution, by allowing more than one instance to match, provided there is a most specific one.


There is also the deprecated extension IncoherentInstances which allows multiple matching instances even if one isn't strictly more specific than another, but not even that can be used here, since both instances are structurally identical.

IncoherentInstances will arbitrarily choose one of the instances, so it should be avoided as much as possible and only ever be used if it doesn't matter which instance is matched.


Edit: One way to reduce boilerplate when defining the instances is to use DerivingVia together with a newtype wrapper:

newtype UsingLTextify a = UsingLTextify { unwrapLT :: a }
newtype UsingTextify  a = UsingTextify  { unwrapT  :: a }

instance LTextify a => Stringify (UsingLTextify a) where
  stringify = L.unpack . ltextify . unwrapLT
instance Textify a  => Stringify (UsingTextify a) where
  stringify = T.unpack . textify . unwrapT

And then you can use it to derive instances like this

{-# LANGUAGE DerivingVia #-}

data Foo = ...
  deriving Stringify via UsingLTextify Foo
instance LTextify Foo where ...

If you do not have control over the data types and don't want to add orphan instances (which you should avoid if possible), these newtypes can also be used directly by wrapping them around your data to give it the relevant instances.