I'm relatively new to Haskell and having trouble understanding the utility of bifunctors. I think I understand them in theory: say for example if I wanted to map across a type that abstracts multiple concrete types, such as Either or Maybe, I'd need to encapsulate them in a bifunctor. But on the one hand, those examples seems particularly contrived, and on the other it does seem like you could achieve that same functionality simply through composition.
As an example, I came across this code in The Essence of the Iterator Pattern by Jeremy Gibbons and Bruno C. d. S. Oliveira:
import Data.Bifunctor
data Fix s a = In {out::s a (Fix s a) }
map' :: Bifunctor s => (a -> b) -> Fix s a -> Fix s b
map' f = In . bimap f (map' f) . out
fold' :: Bifunctor s => (s a b -> b) -> Fix s a -> b
fold' f = f . bimap id (fold' f) . out
unfold' :: Bifunctor s => (b -> s a b) -> b -> Fix s a
unfold' f = In . bimap id (unfold' f) . f
I understand the point is to compose mapping and folding functions to create an iterator pattern and this is achieved by defining a data constructor that requires two parameters. But in practice I don't understand how this is any different then using a regular functor and composing the functions with fmap instead of bimap. I think I clearly must be missing something, both with this example and, likely, in general.
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.
Bifunctors have a kind of* -> * -> *and both parameters can be covariantly mapped over. Compare this to regularFunctors, which have only one parameter (f :: * -> *) which can be covariantly mapped over.For example, think about
Either's usualFunctorinstance. It only allows you tofmapover the second type parameter -Rightvalues get mapped,Leftvalues stay as they are.However, its
Bifunctorinstance allows you to map both halves of the sum.Likewise for tuples:
(,)'sFunctorinstance allows you to map only the second component, butBifunctorallows you to map both parts.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.Using the standard functorial
Fix, as I have above, there's no generic derivation of an instance ofFunctorforList, becauseFixdoesn't know anything aboutList'saparameter. That is, I can't write anything likeinstance Something f => Functor (Fix f)becauseFixhas the wrong kind. I have to hand-crank amapfor lists, perhaps usingcata:The bifunctorial version of
Fixdoes permit an instance ofFunctor.Fixuses one of the bifunctor's parameters to plug in the recursive occurrence ofFix f aand the other one to stand in for the resultant datatype's functorial parameter.So we can write:
and get the
Functorinstance for free: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.