Typeclasses - Bifunctor
2019-01-13
Let's talk about things that could be one thing, or indeed could be another thing altogether.
data Things a b = This a | That b
This could be a This with an a inside, like a String.
thisEgg :: Things String Int
thisEgg = This "Egg"
Or indeed a That with a b inside, like an Int.
thatNumber :: Things String Int
thatNumber = That 68
Now hopefully you are now thinking - "Oh please, I do hope we map a function over one of these values soon" - and worry not, we absolutely bloody can.
Mappity Mappity Map
Let's really push the boat out, and add one to the value inside.
First we'll need a Functor instance. Hopefully nothing too surprising here.
instance Functor (Things a) where
fmap f (That b) = That (f b)
fmap _ (This a) = This a
Now we can map away to our heart's content:
biggerNumber :: Things String Int
biggerNumber = fmap addOne (That 68)
-- biggerNumber == That 69
Nice.
But what about `This "Egg"``? I'd like to get at that egg. Perhaps eat it.
eat :: String -> String
eat s = "The " ++ s ++ " was delicious!"
Can we do that with Functor?
doesntWork :: Things String Int
doesntWork = fmap eat thisEgg
-- ERROR: Couldn't match type β[Char]β with βIntβ
I'm afraid not. Looking back at our Functor instance we can see that the fmap function only lets us map over the values inside That, leaving poor This and our lonely egg very much map-less. But fear not! We have another weapon at hand that will let us get at it.
Enter....Bifunctor!
(cue lightning, thunder, explosions and sounds of a large crowd who are clearly quite impressed).
Definition
Let's ask ghci what's up.
> import Data.bifunctor
> :i Bifunctor
class Bifunctor (p :: * -> * -> *) where
first :: (a -> b) -> p a c -> p b c
second :: (b -> c) -> p a b -> p a c
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
{-# MINIMAL bimap | first, second #-}
Ok. Three functions in here, and it looks like we can make something a Bifunctor by implementing instances of both first and second or just bimap.
Let's take a look at them.
-
first :: (a -> b) -> p a c -> p b c- this takes aBifunctorthat may contain someaandcvalues, and a function that turns anainto some sort ofb. It then runs the function on theavalue, turning it into abvalue. Sort of like doing anfmapover theainsideThisfrom earlier. Pretty nice. Tl;dr - it'sfmapbut over the left value. -
second :: (b -> c) -> p a b -> p a c- this takes aBifunctorwith anaand aband a function that turns thebvalues intocvalues. In the case of ourThingsdatatype ofThisandThat, this let's us get at theThatvalues, which we could anyway so big whoop. Tl;dr - it's our palfmapagain. -
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d- this takes aBifunctorthat may containaandcvalues, and runs a function over both sides. It's doingfirstandsecondat the same time.
OK. If you understand Functor there's hopefully nothing out of the ordinary going on here. Let's slop an instance together and get to work on that delicious egg.
Instances
Laziness dictates that we should define bimap because it is one function instead of two.
instance Bifunctor Things where
bimap f _ (This a) = This (f a)
bimap _ g (That b) = That (g b)
Seems fairly sensible hopefully. Let's give it a spin.
delicious :: Things String Int
delicious = first eat (This "Egg")
-- delicious = This "The Egg was delicious!"
Hooray! Although we defined bimap we got first for free, and that egg was pretty nice.
We can still map over the right hand value too!
doesWork :: Things String Int
doesWork = second addOne (That 68)
-- doesWork == That 69
Again, nice.
Tuples, Pooples
Although our Things example is about sum types, we can also use it on product types like a Tuple, and use Bifunctor to mess with either value as we please.
twoThings :: (Int, String)
twoThings = (100, "Dogs")
Now, we could go ahead and show you first and second but I think you can work out what's going to happen, so let's go absolutely bonkers and race straight to bimap.
(but first, a helper function. Nothing untoward - it merely returns the first thing you give it and ignores the second.)
myConst :: a -> b -> a
myConst a _ = a
Now we can turn our Tuple into a bestselling novel.
oneBestSeller :: (Int, String)
oneBestSeller = bimap (+1) (myConst "Dalmations") twoThings
-- oneBestSeller = (101, "Dalmations")
I Bet You Did Not See That Coming.
For a bonus point, why not try and define first and second for Tuple types using bimap and myConst? Go on. You'll have a great time, I absolutely promise.
That's all, folks
So although helpful with Tuple and Either types, Bifunctor isn't particularly mindblowing, but comes into it's own when we combine it with Contravariant to make Profunctor. More on that in the future though!
Further reading: