This post assumes prior knowledge of - Contravariant - Bifunctor
Why
We've seen how types of kind * -> *
can have
instances for Functor
or Contravariant
,
depending on the position of the type argument. We have also seen that
types of kind * -> * -> *
can have
Bifunctor
instances. These types are morally
Functor
in both type arguments. We're left with one very
common type which we can't map both arguments of:
a -> b
. It does have a Functor
instance for
b
, but the a
is morally
Contravariant
(so it can't have a Bifunctor
instance). This is where Profunctor
s come in.
Here's a list of a few common types with the instances they allow:
Type | Functor |
Bifunctor |
Contravariant |
Profunctor |
---|---|---|---|---|
Maybe a |
✓ | |||
[a] |
✓ | |||
Either a b |
✓ | ✓ | ||
(a,b) |
✓ | ✓ | ||
Const a b |
✓ | ✓ | ||
Predicate a |
✓ | |||
a -> b |
✓ | ✓ |
Although there are some exceptions, you will usually see
Contravariant
or Profunctor
instances over
function types. Predicate
itself is a newtype over
a -> Bool
, and so are most types with these
instances.
Let's take a closer look at a -> b
. We can easily map
over the b
, but what about the a
? For example,
given showInt :: Int -> String
, what do we need to
convert this function to showBool :: Bool -> String
:
showInt :: Int -> String
showInt = show
showBool :: Bool -> String
= _help showBool b
We would have access to: - showInt :: Int -> String
-
b :: Bool
and we want to use showInt
, so we
would need a way to pass b
to it, which means we'd need a
function f :: Bool -> Int
and then _help
would become showInt (f b)
.
But if we take a step back, in order to go from
Int -> String
to Bool -> String
, we need
Bool -> Int
, which is exactly the
Contravariant
way of mapping types.
Exercise 1: Implement a mapInput
function
like:
mapInput :: (input -> out) -> (newInput -> input) -> (newInput -> out)
Extra credit: try a pointfree implementation as
mapInput = _
.
Exercise 2: Try to guess how the Profunctor
class looks like. Look at Functor
,
Contravariant
, and Bifunctor
for
inspiration.
class Profunctor p where
Exercise 3: Implement an instance for ->
for
your Profunctor
class.
instance Profunctor (->) where
How
Unlike Functor
, Contravariant
, and
Bifunctor
, the Profunctor
class is not in
base
/Prelude
. You will need to bring in a
package like profunctors
to access it.
class Profunctor p where
{-# MINIMAL dimap | lmap, rmap #-}
dimap :: (c -> a) -> (b -> d) -> p a b -> p c d
lmap :: (c -> a) -> p a b -> p c b
rmap :: (b -> c) -> p a b -> p a c
dimap
takes two functions and is able to map both
arguments in a type of kind * -> * -> *
.
lmap
is like mapInput
. second
is
always the same thing as fmap
.
Exercise 4: implement dimap
in terms of
lmap
and rmap
.
Exercise 5: implement lmap
and
rmap
in terms of dimap
.
Exercise 6: implement the Profunctor
instance
for ->
:
instance Profunctor (->) where
-- your pick: dimap or lmap and rmap
Exercise 7: (hard) implement the Profunctor
instance for:
data Sum f g a b
= L (f a b)
| R (g a b)
instance (Profunctor f, Profunctor g) => Profunctor (Sum f g) where
Exercise 8: (hard) implement the Profunctor
instance for:
newtype Product f g a b = Product (f a b, g a b)
instance (Profunctor f, Profunctor g) => Profunctor (Product f g) where