Derived data family instance

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 MyOpenFamily provided a Generic instance can be derived.
  • No need to declare an implementation for blah provided a Generic instance 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 -> Type and type families can’t be partially applied,
  • a default clause type MyOpenFamily a = MyOpenFamily (Rep a ()) in a type family makes it closed.

No that’s not a useful comparison, see paragraph just before here. A Data Family is a function on type(s), returning Constructor(s) with argument(s). Data Familys cannot be closed, necessarily are open in the sense you can freely add instances providing they don’t overlap, see that link “type safety”. IOW what you’re trying to do is exactly what DFs are deliberately designed to avoid.

How do you think a default instance could declare a Constructor? And wouldn’t it be better for the compile to fail rather than seem to select an instance that’s not usable at runtime?

Note the default only gets activated providing there is at least an explicit instance declared for the type — even if it’s no more than the head. That doesn’t seem to fit what you describe. A Data Family at least needs a Constructor.

It seems you’re expecting separate modules/separate compilation. Then I suggest you first get your head around ‘orphan instances’, and assure yourself they could never happen in your scenario.

Thanks for attempting to talk me off this route.
It would be cool if the (data) instances were “just present” without any statement by the library user but that is perhaps indeed asking for trouble, i.e. overlap hell.

Instead, can the DerivingVia trick be extended to data families? After all, deriving Generic also defines a type family instance for your type without any further intervention or guidance. Apparently we can’t. When the data family is part of the class we want to derive:

    • Can't make a derived instance of
        ‘Blah Foo’ with the via strategy:
        the class has associated data types
    • In the data declaration for ‘Foo’

When the data family is not part of the class:

   • Couldn't match type ‘NewtypeWrapped Foo’ with ‘Foo’
        arising from the coercion of the method ‘blah’
          from type ‘MyOpenFamily (NewtypeWrapped Foo) -> Blubb’
            to type ‘MyOpenFamily Foo -> Blubb’
    • When deriving the instance for (Blah Foo)

which is fair enough, because I did not explicity tell the compiler that I intended MyOpenFamily Foo and MyOpenFamily (NewtypeWrapped Foo) to be coercible.

Emm, it’s not as if it’s a matter of persuading: you’re asking for something that’s not supported, and never will be, because it breaks type safety — see 6.4.10.1.3 in the docos (link now in my earlier message). Possibly you need to backtrack over how you got down this rabbit-hole/ what are you trying to achieve.

Coming back to re-read the thread, I think my reply was rather muddled. I’ve revised quite a bit.

Maybe because your q is rather muddled: the title is about Data Family instances, but you give no example decls. The sample code seems to be only methods. Can you give one data decl for a non-overlapped instance.

I realized my original wording is indeed muddled/misleading. The convenience mentioned in the OP may be too strong a requirement. What I am after is not a globally default data family instance, but an automatically derived instance, given some constraint context. I have updated the thread title accordingly. This may still be impossible, because as @AntC2 points out, the problem of unique constructor names in data families persists.

The problem

We want to associate with types a a type F a that supports some operations f. Given a certain context Ctxt a, certain constructors for F a are sensible and a generic implementation f_ctxt :: Ctxt a => F a -> ... is possible. We’d like to enable the library user so declare, with minimum boilerplate, for a concrete type A with Ctxt A instance

  1. that the type F A should have the canonical constructors that the context suggests,
  2. that the implementation of f should be the generic f_ctxt implementation,
  3. while retaining the option to provide alternative implementations of F A and f, e.g. when no Ctxt instance exists.

Type family pointing to dedicated types

This is the approach taken e.g. by GHC.Generics. Consider a class with associated type family:

class C a where
  type F a :: Type
  f :: F a -> (a -> Bool)

We’d like F to be a data family, but let’s introduce the constructors as dedicated data types instead.

newtype Lt a = Lt a       -- type instance constructor
newtype Upper a = Upper a -- wrapper directing instance resolution

instance Ord a => C (Upper a) where
    type F (Upper a) = Lt a
    f (Lt a) (Upper b) = a < b

For an existing data type, we can now choose to have a C instance where F has the Lt constructor, and this constructor is shared across potentially many C a instances

deriving via (Upper Integer) instance (C Integer)
-- needs UndecidableInstances

just like many Generic enumeration types share the :+: constructor in their Rep instances.

Data family with sharing of constructors

Compare the above to essentially the same class with open data family.

data family F a
class C a where
    f :: F a -> (a -> Bool)

data instance F (Upper a) = Lt a

instance Ord a => C (Upper a) where
    f (Lt a) (Upper b) = a < b

Again, we can choose to implement instances for concrete types using Upper as directing wrapper:

data instance F Integer = IntUpper !(F (Upper Integer))
instance C Integer where
    f (IntUpper lt) b = f lt (Upper b)

We can even re-use the Lt constructor in more elaborate data instances.

data instance F Bool = BoolUpper (F (Upper Bool)) | Equals Bool
instance C Bool where
   f (BoolUpper lt) b = f lt (Upper b)
   f (Equals a) b = a == b

What does not work, however, is to make the wrapper IntUpper invisible and derive the C instance without boilterplate.

-- declaring
deriving via (Upper Integer) instance (C Integer)
-- fails with 
-- Couldn't match type ‘Upper Integer’ with ‘Integer’
--        arising from a use of ‘GHC.Prim.coerce’

I see no reason why this could not be done, other than the compiler being unable to coerce the types

F (Upper Integer) 
F        Integer

This is apparently known behaviour.

Conclusion

  • Due to parameters of data families having type role nominal by default, we can not use DerivingVia to derive class instances involving data families via coerce.
  • The idea of a default and overlappable family instance is both dangerous and doomed because a data family constructor is supposed to identify the type parameter of the family.
  • The best we can currently do is what GHC.Generics does and provide the constructors for type family instances in separate, dedicated data types. The usual limitations apply, e.g. no partial application with type families.

I don’t pretend to understand what you’re trying to do, but if you want a Generic/auto-derived approach, is a Data Family helping?

  • Is it that each instance needs more than one Constructor?
  • Do different instances need differing numbers of arguments to each Constructor?

Why couldn’t this be a basis?:

type family F a :: Type

class Ctxt a  where
    — methods

data TG a  where
    MkTG :: Ctxt a => (F a) -> TG a