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 Profunctors 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
showBool b = _helpWe 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 whereExercise 3: Implement an instance for -> for
your Profunctor class.
instance Profunctor (->) whereHow
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 cdimap 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 rmapExercise 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) whereExercise 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