====== 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]) :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: - if we get a ''Nothing'', we return a ''Nothing'' - 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 ''Maybe''s 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''.