Functors are data types over which we can map
a function (that is, apply a function on each element while keeping the overall structure). You've already encountered a Functor, namely haskell lists. For historical reasons, map
only works on lists (map :: (a -> b) -> [a] -> [b]
), but there exists a more general function fmap
(fmap :: (Functor f) => (a -> b) -> f a -> f b
).
The class definition looks like this:
class Functor f where fmap :: (a -> b) -> f a -> f b
Notice (based on the types f a
and f b
) that what is enrolled in Functor
is not a data type, but a type constructor with kind * -> *
. Thus we say that []
, Maybe
are functors, and not [Int]
, [a]
, Maybe Char
, Maybe a
etc.
Can a pair (,)
be a functor? The answer is no, because the type constructor has kind (,) :: * -> * -> *
. However, we can partially apply to a specific type and make the resulting * -> *
constructor a Functor
. The haskell syntax for this is as follows (this already exists in Prelude
):
instance Functor ((,) a) where fmap f (x, y) = (x, f y)
How are Functors
useful in practice?
Let's consider a function which sums the first and last elements of a list:
sumExtremities :: [Int] -> Int sumExtremities l = head l + last l
We can then combine it with other functions as usual:
*Main> :{ *Main| foo :: Int -> Bool *Main| foo x = 2 * x > 3 *Main| :} *Main> foo (sumExtremities [1,2,3]) True
However, we notice that our function does not work well with empty lists:
*Main> sumExtremities [] *** Exception: Prelude.head: empty list
We change our function so that it does not raise an error, but a more elegant Nothing
.
sumExtremities :: [Int] -> Maybe Int sumExtremities [] = Nothing sumExtremities l = Just (head l + last l)
However, now we can no longer combine our function with other Int
functions as before:
*Main> :{ *Main| foo :: Int -> Bool *Main| foo x = 2 * x > 3 *Main| :} *Main> foo (sumExtremities [1,2,3]) <interactive>:24:6: error: • Couldn't match expected type ‘Int’ with actual type ‘Maybe Int’ • In the first argument of ‘foo’, namely ‘(sumExtremities [1, 2, 3])’ In the expression: foo (sumExtremities [1, 2, 3]) In an equation for ‘it’: it = foo (sumExtremities [1, 2, 3])
We should adapt our foo
to take a Maybe Int
instead. But what if it gets a Nothing
? foo
tells us if the double of its argument is greater than 3; this question does not make sense for Nothing
, so we cannot return any boolean value. So we should also change the return type to be Maybe Bool
.
*Main> :{ *Main| foo :: Maybe Int -> Maybe Bool *Main| foo Nothing = Nothing *Main| foo (Just x) = Just (2 * x > 3) *Main| :} *Main> foo (sumExtremities [1,2,3]) Just True
Now our functions work and can handle the empty list without throwing an exception. However, it should be obvious that we now need to change any Int -> a
function we want to apply on our result to a Maybe Int -> Maybe a
function, which seems like a lot of work. Plus the change will be as dull in all cases:
Nothing
, we return a Nothing
Just
, we unpack it, do with it what the function did originally, then repack it in a Just
.
It would be great if we could just have a function which does this transformation for us. And we do. Remember currying and how multiple-argument functions actually take on argument and return another function and so on. Thus we can reinterpret fmap
's signature as: (a -> b) -> (f a -> f b)
. That is: it takes a function from a
to b
and returns a function from a functor a
to a functor b
. Set the functor f
to Maybe
and that's it!
*Main> :{ *Main| foo :: Int -> Bool *Main| foo x = 2 * x > 3 *Main| :} *Main> fmap foo (sumExtremities [1,2,3]) Just True
Now we can use our function foo
on Maybe
s without manually changing its definition.
1. Define a data type Tree
that models a tree with an arbitrary number of children (use a list).
2. Enroll Tree
into Functor
.
3. Enroll Tree
into Foldable
.
4. Define a Zippable
class which provides a single function zipp
, similar with zipWith
, but not only for lists, but for any type constructor of kind * → *
.
5. Enroll haskell lists into Zippable
.
6. Enroll Maybe
into Zippable
.
7. Enroll Either a
into Zippable
.
8. Enroll the binary tree data type into Zippable
.
9. Enroll Tree
into Zippable
.
10. Enroll ((,) a)
into Zippable
.
11. Enroll (→) a
into Zippable
.