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
= This "Egg" thisEgg
Or indeed a That
with a b
inside, like an Int
.
thatNumber :: Things String Int
= That 68 thatNumber
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
= fmap addOne (That 68)
biggerNumber -- biggerNumber == That 69
Nice.
But what about `This “Egg”``? I’d like to get at that egg. Perhaps eat it.
eat :: String -> String
= "The " ++ s ++ " was delicious!" eat s
Can we do that with Functor
?
doesntWork :: Things String Int
= fmap eat thisEgg
doesntWork -- 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 aBifunctor
that may contain somea
andc
values, and a function that turns ana
into some sort ofb
. It then runs the function on thea
value, turning it into ab
value. Sort of like doing anfmap
over thea
insideThis
from earlier. Pretty nice. Tl;dr - it’sfmap
but over the left value.second :: (b -> c) -> p a b -> p a c
- this takes aBifunctor
with ana
and ab
and a function that turns theb
values intoc
values. In the case of ourThings
datatype ofThis
andThat
, this let’s us get at theThat
values, which we could anyway so big whoop. Tl;dr - it’s our palfmap
again.bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
- this takes aBifunctor
that may containa
andc
values, and runs a function over both sides. It’s doingfirst
andsecond
at 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
This a) = This (f a)
bimap f _ (That b) = That (g b) bimap _ g (
Seems fairly sensible hopefully. Let’s give it a spin.
delicious :: Things String Int
= first eat (This "Egg")
delicious -- 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
= second addOne (That 68)
doesWork -- 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)
= (100, "Dogs") twoThings
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
= a myConst a _
Now we can turn our Tuple
into a bestselling novel.
oneBestSeller :: (Int, String)
= bimap (+1) (myConst "Dalmations") twoThings
oneBestSeller -- 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: