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.
CodePudding user response:
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
class (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.