Table of Contents

7. Functors, Foldables

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?

1. Example: the Maybe functor

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:

  1. if we get a Nothing, we return a Nothing
  2. if we get a 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 Maybes without manually changing its definition.

Exercises

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.