This post assumes prior knowledge of - the Functor
class
/ concept - the functor instance for Either a
,
(,) a
- basic kind knowledge, e.g. the difference between
* -> *
and * -> * -> *
Why
In Haskell, functors can only be defined for types of kind
* -> *
like Maybe a
or [a]
.
Their instances allow us to use fmap
(or
<$>
) to go from Maybe a
to
Maybe b
using some a -> b
, like:
> show <$> Just 1
λJust "1"
> show <$> Nothing
λNothing
> show <$> [1, 2, 3]
λ"1", "2", "3"]
[
> show <$> []
λ []
We can even define functor instances for higher kinded types, as long
as we fix type arguments until we get to * -> *
. For
example, Either
has kind * -> * -> *
,
but Either e
has kind * -> *
. So that means
that we can have a functor instance for Either e
, given
some type e
. This might sound confusing at first, but all
it means is that the e
cannot vary, so we can go from
Either e a
to Either e b
using some
a -> b
, but we cannot go from Either e1 a
to Either e2 a
or Either e2 b
even if we had
both a -> b
and e1 -> e2
. How would we
even pass two functions to fmap
?
> show <$> Right 1
λRight "1"
> show <$> Left True
λLeft True
In the first example, we go from Either a Int
to
Either a String
using
show :: Int -> String
. In the second example, we go from
Either Bool a
to Either Bool String
, where
a
needs to have a Show
instance.
But what if we want to go from Either a x
to
Either b x
, given some a -> b
?
Let's see how we could implement it ourselves:
mapLeft :: (a -> b) -> Either a x -> Either b x
Left a) = Left (f a)
mapLeft f (= r mapLeft _ r
Since we are trying to map the Left
, the interesting bit
is for that constructor: we apply f
under
Left
. Otherwise, we just leave the value as-is; a
Right
value of type x
(we could have written
mapLeft _ (Right x) = Right x
and it would work the
same).
Here's a few warm-up exercises. The first uses typed holes to guide
you and clarify what's meant by "using either
". The last
exercise can be a bit tricky. Look up what Const
is and use
typed holes.
Exercise 1: re-implement mapLeft'
using
either
:
mapLeft' :: (a -> b) -> Either a x -> Either b x
= either _leftCase _rightCase e mapLeft' f e
Exercise 2: implement mapFirst
:
mapFirst :: (a -> b) -> (a, x) -> (b, x)
Exercise 3: implement remapConst
:
import Data.Functor.Const (Const(..))
remapConst :: (a -> b) -> Const a x -> Const b x
How
While we can implement mapLeft
, mapFirst
,
and remapConst
manually, there is a pattern: some types of
kind * -> * -> *
allow both their type arguments to
be mapped like a Functor
, so we can define a new class:
class Bifunctor p where
{-# MINIMAL bimap | first, second #-}
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
first :: (a -> b) -> p a c -> p b c
second :: (b -> c) -> p a b -> p a c
bimap
takes two functions and is able to map both
arguments in a type of kind * -> * -> *
.
first
is a lot like the functions we just defined manually.
second
is always the same thing as fmap
. This
class exists in base
, under
Data.Bifunctor
.
Exercise 4: implement bimap
in terms of
first
and second
.
Exercise 5: implement first
and
second
in terms of bimap
.
Exercise 6: implement the Bifunctor
instance
for Either
:
instance Bifunctor Either where
Left a) = _leftCase
bimap f _ (Right b) = _rightCase bimap _ g (
Exercise 7: Implement the Bifunctor
instance
for tuples (a, b)
.
Exercise 8: Implement the Bifunctor
instance
for Const
.
Exercise 9: Implement the Bifunctor
instance
for (a, b, c)
.
Exercise 10: Find an example of a type with kind
* -> * -> *
that cannot have a Bifunctor
instance.
Exercise 11: Find an example of a type with kind
* -> * -> *
which has a Functor
instance
when you fix one type argument, but can't have a Bifunctor
instance.