Related question from 2023: Choosing data representation based on type
As far as I understand, a GADT is like a closed data family. I want to construct an open data family, where the library user can add her own instances for concrete types that weren’t mentioned in the library. The data family is associated with a type class. Thanks to DefaultSignatures, we can provide default implementations for ordinary class methods like so:
class Blah x where
blah :: x -> Blubb
default blah :: Generic x => x -> Blubb
blah = -- implementation using the context
Likewise, I would like to create a default or overlappable data family instance, but it seems to be rejected for reasons explained in the thread linked to above. Assume we have a data family MyOpenFamily and we are able to use MyOpenFamily x and MyOpenFamily (Rep x ()) interchangingly. The goal is to default
class Blah x where
blah :: MyOpenFamily x -> Blubb
I would like to give the library user the same convenience as with DefaultSignatures.
- No need to declare an instance of
MyOpenFamilyprovided aGenericinstance can be derived. - No need to declare an implementation for
blahprovided aGenericinstance can be derived.
Ugly and potentially circular work-around (related to the Generically trick): Further wrap the open data family in yet another GADT with two constructors, like so:
data MyFamilyWrapper x where
DirectInstance :: MyOpenFamily x -> MyFamilyWrapper x
ViaGeneric :: (Generic x, f ~ Rep x) =>
MyOpenFamily (f ()) -> MyFamilyWrapper x
Then change the type of blah:
class Blah where
blah :: MyFamilyWrapper x -> Blubb
For non-Generic types, the user provides a definition of blah that pattern-matches only on DirectInstance. Thanks to the constraint on ViaGeneric, we know that is a total definition. For Generic types, we can then have a default implementation:
default blah :: (Generic x, Rep x ~ f, Blah (f ())) => MyFamilyWrapper x -> Blubb
blah (ViaGeneric p) = blah (DirectInstance p)
I wonder whether there is a more concise way of doing this, without MyFamilyWrapper. Using ordinary type families is out of the question because
- the real class involves kinds
Type -> Typeand type families can’t be partially applied, - a default clause
type MyOpenFamily a = MyOpenFamily (Rep a ())in a type family makes it closed.