I think you're overthinking it slightly. A bifunctor is just like a two-parameter functor. Gibbons and Oliveira's idea is just one application of bifunctors, just like the standard zoo of recursion schemes is just one application of functors.
class Bifunctor f where
bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
Bifunctor
s have a kind of * -> * -> *
and both parameters can be covariantly mapped over. Compare this to regular Functor
s, which have only one parameter (f :: * -> *
) which can be covariantly mapped over.
For example, think about Either
's usual Functor
instance. It only allows you to fmap
over the second type parameter - Right
values get mapped, Left
values stay as they are.
instance Functor (Either a) where
fmap f (Left x) = Left x
fmap f (Right y) = Right (f y)
However, its Bifunctor
instance allows you to map both halves of the sum.
instance Bifunctor Either where
bimap f g (Left x) = Left (f x)
bimap f g (Right y) = Right (g y)
Likewise for tuples: (,)
's Functor
instance allows you to map only the second component, but Bifunctor
allows you to map both parts.
instance Functor ((,) a) where
fmap f (x, y) = (x, f y)
instance Bifunctor (,) where
bimap f g (x, y) = (f x, g y)
Note that Maybe
, which you mentioned, doesn't fit into the framework of bifunctors because it has only one parameter.
On the question of Fix
, the fixed point of a bifunctor allows you to characterise recursive types which have a functorial type parameter, such as most container-like structures. Let's use lists as an example.
newtype Fix f = Fix { unFix :: f (Fix f) }
data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)
Using the standard functorial Fix
, as I have above, there's no generic derivation of an instance of Functor
for List
, because Fix
doesn't know anything about List
's a
parameter. That is, I can't write anything like instance Something f => Functor (Fix f)
because Fix
has the wrong kind. I have to hand-crank a map
for lists, perhaps using cata
:
map :: (a -> b) -> List a -> List b
map f = cata phi
where phi Nil_ = Fix Nil_
phi Cons_ x r = Fix $ Cons_ (f x) r
The bifunctorial version of Fix
does permit an instance of Functor
. Fix
uses one of the bifunctor's parameters to plug in the recursive occurrence of Fix f a
and the other one to stand in for the resultant datatype's functorial parameter.
newtype Fix f a = Fix { unFix :: f a (Fix f a) }
instance Bifunctor f => Functor (Fix f) where
fmap f = Fix . bimap f (fmap f) . unFix
So we can write:
deriveBifunctor ''ListF
type List = Fix ListF
and get the Functor
instance for free:
map :: (a -> b) -> List a -> List b
map = fmap
Of course, if you want to work generically with recursive structures with more than one parameter then you need to generalise to tri-functors, quad-functors, etc... This is clearly not sustainable, and plenty of work (in more advanced programming languages) has been put into designing more flexible systems for characterising types.
class Covariant (n :: Nat) p
expressing thatp
is covariant in itsnth
parameter, but I'm not quite sure what that would look like. – Bengali