Lysxia / generic-data

Generic data types in Haskell, utilities for GHC.Generics
https://hackage.haskell.org/package/generic-data
MIT License
44 stars 9 forks source link

Generic deriving Bi* instances #23

Closed identicalsnowflake closed 3 years ago

identicalsnowflake commented 5 years ago

Here's a sketch implementation of deriving Bifunctor, Bifoldable, and Bitraversable instances adapted from @kcsongor's https://kcsongor.github.io/generic-deriving-bifunctor/.

You can use it like this:

data MyBi a b = MyBi String a (Either a b) | Int (Maybe b)
  deriving (Generic)

instance Bifunctor MyBi where
  bimap = gbimap

instance Bifoldable MyBi where
  bifoldMap = gbifoldMap

instance Bitraversable MyBi where
  bitraverse = gbitraverse

Unfortunately, I wasn't able to get anything analogous to the existing "generically" pattern deriving Bifunctor via (Generically2 MyBi). Maybe someone else will have more luck.

Another thing I don't like is that the implementation only handles 1 layer of nested functors, which feels a bit hacky. For example, the following will not work due to the two layers of nested functors [ Maybe a ]:

data MyBi a b = MyBi [ Maybe a ] b
  deriving (Generic)

instance Bifunctor MyBi where
  bimap = gbimap

This is disappointing, but my quick attempts to hack around it by deriving a functor instance on the fly didn't pan out. Maybe there's some way to trick instance search into collapsing functor layers together with composition? In any case, the TH implementation in the bifunctors package does handle these structures with nested functors.

That said, despite these shortcomings, here is one place where the generic approach here does win out over TH:

data MyBi (f :: Symbol -> * -> *) a b = MyBi (f "" a) (f "" b)
  deriving (Generic,Generic1)

instance (forall s . Functor (f s)) => Bifunctor (MyBi f) where
  bimap = gbimap

This (with kitchen sink extensions) will compile, while $(deriveBifunctor ''MyBi) will bail.

Lysxia commented 5 years ago

Thanks a lot!

I always felt uneasy about the incoherent instances, but in this particular case of deriving Bifunctor and related classes, you actually have the guarantee that "if it typechecks it works"!

For the matter of nested functors, you can try to refactor the logic to match various combinations of a,b,c,d in a separate class where all six indices have kind Type. In the instances where it finds a type application with a (bi)functor f, it can then simply use a constraint to recurse; that even supports contravariant mappings to some extent with instances for (->).

identicalsnowflake commented 5 years ago

For the matter of nested functors, you can try to refactor the logic to match various combinations of a,b,c,d in a separate class where all six indices have kind Type. In the instances where it finds a type application with a (bi)functor f, it can then simply use a constraint to recurse; that even supports contravariant mappings to some extent with instances for (->).

Yeah it does seem like there should be a way to get it to recurse, probably with a base default case where no functor is found and an overlapping case to iterate on while we have a matching functor. Might give it a shot later.

anka-213 commented 3 years ago

I think the solution to allow deriving Bifunctor via (Generically2 MyBi) to work is to use quantified constraints, which is a recent GHC extension that allows us to write forall inside constraints:

{-# LANGUAGE QuantifiedConstraints #-}
...

newtype Generically2 f a b = Generically2 { unGenerically2 :: f a b }

instance ( forall a b. Generic (f a b)
         , forall a b c d s t. (s ~ Rep (f a c), t ~ Rep (f b d)) => GBifunctor s t a b c d
         -- ,  forall a b c d. GBifunctor (Rep (f a c)) (Rep (f b d)) a b c d
         ) =>
         Bifunctor (Generically2 f) where
    bimap f g = Generically2 . to . gbimap' f g . from . unGenerically2

The equality constraints s ~ Rep (f a c) are used because quantified constraints doesn't support type families directly: https://gitlab.haskell.org/ghc/ghc/-/issues/14860#note_149720 Otherwise we could have used the simpler commented out version.

anka-213 commented 3 years ago

Here are the rest of the instances:


instance ( forall a b. Generic (f a b)
         , forall a b s m. (s ~ Rep (f a b)) => GBifoldable s a b m
         ) =>
         Bifoldable (Generically2 f) where
    bifoldMap f g = gbifoldMap' f g . from . unGenerically2

instance ( forall a b. Generic (f a b)
         , forall a b c d s t. (s ~ Rep (f a c), t ~ Rep (f b d)) => GBitraversable s t a b c d
         , Bifunctor (Generically2 f)
         , Bifoldable (Generically2 f)
         ) =>
         Bitraversable (Generically2 f) where
    bitraverse f g = fmap (Generically2 . to) . gbitraverse' f g . from . unGenerically2
Lysxia commented 3 years ago

Somewhat recently I released https://hackage.haskell.org/package/generic-functor which I think incorporates those ideas (including using QuantifiedConstraints) :)