Closed identicalsnowflake closed 3 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 (->)
.
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.
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.
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
Somewhat recently I released https://hackage.haskell.org/package/generic-functor which I think incorporates those ideas (including using QuantifiedConstraints) :)
Here's a sketch implementation of deriving
Bifunctor
,Bifoldable
, andBitraversable
instances adapted from @kcsongor's https://kcsongor.github.io/generic-deriving-bifunctor/.You can use it like this:
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 ]
: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:
This (with kitchen sink extensions) will compile, while
$(deriveBifunctor ''MyBi)
will bail.