Lesson 10 / 25

Functor: Mapping Inside a Context

Understand fmap and the Functor laws with lists, Maybe, Either and your own types.

fmap generalises map

Many types are containers or contexts around values: a list holds many values, Maybe holds zero or one, Either e holds a value or an error, IO will produce a value when run. The Functor class captures the ability to apply a function to the value(s) inside without changing the structure: fmap :: Functor f => (a -> b) -> f a -> f b, with the infix synonym <$>. fmap (+1) (Just 2) is Just 3, fmap (+1) Nothing is Nothing, fmap length (Right "abc") is Right 3, and fmap over IO transforms the result of an action. Instances must obey two laws: mapping id changes nothing (fmap id = id), and mapping a composition equals composing maps (fmap (f . g) = fmap f . fmap g). These laws guarantee fmap only transforms values and never adds, removes or reorders them. You can write instances for your own types, and GHC can derive them with deriving Functor (the DeriveFunctor extension, included in GHC2021). Related helpers: <$ replaces values with a constant, and void discards a result.

fmap over different contexts

The same function is applied inside a list, a Maybe and an Either, keeping their shape.

Three containers of different shapes with small values inside, each passing under the same function arrow and coming out the same shape with transformed values.
Figure 4.1 — fmap preserves structure.

Functor instances in action

Built-in functors and a derived instance for a tree.

{-# LANGUAGE DeriveFunctor #-}

data Tree a = Leaf | Node (Tree a) a (Tree a)
  deriving (Show, Functor)

-- a hand-written instance would be:
-- instance Functor Tree where
--   fmap _ Leaf = Leaf
--   fmap f (Node l x r) = Node (fmap f l) (f x) (fmap f r)

fromList :: Ord a => [a] -> Tree a
fromList = foldr insert Leaf
  where
    insert x Leaf = Node Leaf x Leaf
    insert x t@(Node l y r)
      | x < y     = Node (insert x l) y r
      | x > y     = Node l y (insert x r)
      | otherwise = t

main :: IO ()
main = do
  print (fmap (* 2) [1, 2, 3 :: Int])                   -- [2,4,6]
  print ((+ 1) <$> Just (41 :: Int))                    -- Just 42
  print (length <$> (Right "abc" :: Either String String))   -- Right 3
  print (fmap show (fromList [3, 1, 2 :: Int]))         -- a Tree of Strings
  n <- length <$> getLine                               -- fmap over an IO action
  print n

Gift wrapping

fmap is a helper who can change the gift inside any kind of wrapping (a box, an envelope, a bag that might be empty) without tearing the wrapping or changing how many gifts there are.

Quick check: What is `fmap (+1) Nothing`?

  • Just 1
  • An error
  • Nothing
  • 1
Answer

Nothing — Mapping over Nothing leaves Nothing: there is no value to transform.