Skip to content

Module 07.1: Functors — Generalizing map

In Module 03.1, we saw that map lets us transform the elements of a list without writing the list-traversal code again and again. This module starts with that familiar idea and asks a small question: what if the thing holding our values isn't a list?

By the end of this module, you should be able to:

  • explain what makes a type mappable,
  • read and write simple class and instance declarations,
  • read the Functor typeclass,
  • implement fmap for a binary tree,
  • use fmap with Maybe, and
  • explain the two functor laws.

How to use this module

When you see a box labeled Gradescope question, answer that question in the Module 7.1 Completion assignment on Gradescope.


Remember map

map (+1) [1, 2, 3]      -- [2,3,4]
map show [1, 2, 3]      -- ["1","2","3"]

The two calls transform their elements differently, but neither changes the list's structure. We still have three elements, in the same positions.

map :: (a -> b) -> [a] -> [b]

Recall why map is so useful. We could write separate recursive functions to increment a list, double its elements, or convert them to strings. But all those functions would share the same traversal. map factors out that repeated traversal:

myMap :: (a -> b) -> [a] -> [b]
myMap _ []     = []
myMap f (x:xs) = f x : myMap f xs

The function argument tells us how to change the values. The recursive definition determines how to preserve the structure.


What About a Tree?

Let's use a binary tree that stores a value at each node:

data Tree a
  = Empty
  | Node a (Tree a) (Tree a)
  deriving (Show, Eq)

exampleTree :: Tree Int
exampleTree =
  Node 5
    (Node 2 Empty Empty)
    (Node 7 (Node 1 Empty Empty) Empty)

Its shape is:

       5
      / \
     2   7
        /
       1

We can write a function that transforms every value, while preserving every node and branch:

mapTree :: (a -> b) -> Tree a -> Tree b
mapTree _ Empty = Empty
mapTree f (Node x left right) =
  Node (f x)
       (mapTree f left)
       (mapTree f right)

Try:

mapTree (+10) exampleTree

The result has this shape:

       15
      /  \
     12  17
        /
       11

Notice that our implementation is doing exactly what map did on lists: change the contents, but preserve the arrangement of the containers.

Gradescope question: What does mapping preserve?

Submit this response on Gradescope.

What does mapTree (+10) exampleTree change, and what does it preserve? How is this similar to map (+10) [5,2,7,1]?


Interlude: Writing Your Own Type Class

In Module 03.2 we used type classes such as Eq and Show, and put our own types into them with deriving. Before meeting a new type class, let's see what one looks like when you write it yourself.

Declaring a class

A class declaration names a type class and lists the operations its members must support:

class Describable a where
  describe :: a -> String

Read it as: a type a is Describable if there is a function describe :: a -> String for it. The a is a placeholder for whichever type joins the class. The declaration gives only the type of describe, not its code.

Joining a class: instances

An instance declaration puts a specific type into the class by supplying the code:

data CoinFlip = Heads | Tails

instance Describable CoinFlip where
  describe Heads = "the coin landed heads up"
  describe Tails = "the coin landed tails up"

instance Describable Bool where
  describe True  = "yes"
  describe False = "no"

Each instance fills in a. In the first, a is CoinFlip, so there describe :: CoinFlip -> String. Haskell uses the argument's type to pick which code to run:

describe Heads   -- "the coin landed heads up"
describe False   -- "no"
describe 'x'     -- type error: no instance for Describable Char

That error is the same kind as the "No instance for (Eq ThermostatSetting)" error from Module 03.2: the type simply hasn't joined the class.

Using a class in a type: constraints

shout :: Describable a => a -> String
shout x = describe x ++ "!"

The part before => is a constraint: shout works for any type a, as long as a is Describable. You saw one in Module 03.2 ((/=) :: Eq a => a -> a -> Bool), and you'll see Functor f => in the same position below.

deriving was writing instances for you

Eq is declared the same way (simplified; the real class also includes /=):

class Eq a where
  (==) :: a -> a -> Bool

When you wrote deriving Eq, Haskell generated an instance like this for you:

instance Eq CoinFlip where
  Heads == Heads = True
  Tails == Tails = True
  _     == _     = False

Use one or the other, not both.

Superclasses

Module 03.2 noted that Ord depends on Eq. A class declaration records that with a constraint of its own:

class Eq a => Ord a where
  (<) :: a -> a -> Bool
  -- ...and the other comparisons

Read it as: a type can join Ord only if it is already in Eq. Eq is a superclass of Ord. You'll see this again in Module 07.2.


The Common Pattern Has a Name

Back to map and mapTree. With class declarations in hand, we can name what these two functions share. Compare their types:

map     :: (a -> b) -> [a]    -> [b]
mapTree :: (a -> b) -> Tree a -> Tree b

The only difference is the structure we are mapping over. This suggests a general notion of a mappable structure, and that is exactly what a type class can capture: one operation, with each type supplying its own code (just as describe had separate code for CoinFlip and Bool).

Haskell already has this class. It calls mappable structures functors, and the class's one operation is named fmap:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

This is an ordinary class declaration, like Describable above, with one twist. Here f does not mean the function we're applying. It stands for a type constructor: something like [], Tree, or Maybe, which accepts an element type and makes a complete type.

For instance:

Tree       is a type constructor
Tree Int   is a complete type
Tree Bool  is another complete type

That is why the Functor instance will be for Tree, rather than only for Tree Int. A mapping operation must be able to change the element type, such as from Int to String.

Gradescope question: Read the type

Submit this response on Gradescope.

In your own words, explain this signature:

fmap :: Functor f => (a -> b) -> f a -> f b

Why would Functor (Tree Int) not express the general operation we want?


Make Tree an Instance of Functor

We already know the implementation: it is mapTree, written as an instance declaration, just like instance Describable CoinFlip.

instance Functor Tree where
  fmap _ Empty = Empty
  fmap g (Node x left right) =
    Node (g x)
         (fmap g left)
         (fmap g right)

Now these all work:

fmap (+10) exampleTree  -- Tree Int
fmap even exampleTree   -- Tree Bool
fmap show exampleTree   -- Tree String

The typeclass does not change how the tree is stored. It gives us a common name for an operation we've already discovered. This is another example of abstraction by recognizing and factoring out common structure.


Maybe Is a Functor Too

Remember the essential definition:

data Maybe a = Nothing | Just a

What would it mean to transform a value inside Maybe while preserving its structure?

fmap (+1) (Just 5)   -- Just 6
fmap (+1) Nothing    -- Nothing
fmap show (Just 42)  -- Just "42"

A conceptual implementation is tiny:

-- This instance already exists in Haskell; don't redefine it.
instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap g (Just x) = Just (g x)

Here, “preserving the structure” means preserving the distinction between a value is present and no value is present. We do not have to pattern match every time we want to change a successful result.

Gradescope question: Maybe values

Submit this response on Gradescope.

What are the results of these expressions? Explain briefly why the second does not produce Just anything.

fmap (*3) (Just 4)
fmap (*3) Nothing
fmap (== 42) (Just 0)

The Pattern So Far

We've now seen the same operation in several settings:

Structure What fmap preserves
List Length and position of elements
Tree Nodes and branching shape
Maybe Just versus Nothing

The kind of structure determines how mapping behaves.


Two Simple Laws

A sensible fmap should obey two functor laws.

Identity

fmap id = id

If you do not change any value, the whole structure should stay the same.

Composition

fmap (g . h) = fmap g . fmap h

Changing the values first with h and then with g should be equivalent to changing them once with the combined function g . h.

For example, using a list:

fmap (show . (+1)) [1, 2]
-- ["2","3"]

(fmap show . fmap (+1)) [1, 2]
-- ["2","3"]

These laws are more than style preferences: they express what we mean by a mapping operation that respects the structure.

Gradescope question: Functor laws

Submit this response on Gradescope.

Explain the identity law fmap id = id using a tree or a Maybe. What would be wrong with an implementation of fmap that deleted one tree node even when given id?


A Practical Note: Deriving Functor

For many suitable datatypes, GHC can generate this instance automatically:

{-# LANGUAGE DeriveFunctor #-}

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

This is convenient, but understanding the earlier hand-written instance explains what is being generated. (Use either the derived instance or the manual one, not both.)


What Comes Next?

Everything we've done so far involves functions with this shape:

a -> b

They transform ordinary values, and fmap handles the surrounding structure.

But some useful functions produce a structured result themselves:

safeSqrt :: Double -> Maybe Double

Can we compose functions like that without repeatedly unpacking and repacking Maybe values? That is our problem for Module 07.2.


Finish Module 07.1

Make sure you have answered the four Gradescope questions embedded above:

  1. What does mapping change, and what does it preserve?
  2. What does the type of fmap say, and why is the instance for Tree rather than Tree Int?
  3. How does fmap behave on Just and Nothing?
  4. What does the identity law fmap id = id require?

Then answer these two final questions.

Gradescope question: Time spent

Submit this response on Gradescope.

Approximately how much time did you spend on this module?

  • less than 30 minutes
  • 30 - 45 minutes
  • 45 - 60 minutes
  • 60 - 75 minutes
  • 75 - 90 minutes
  • more than 90 minutes

Gradescope question: Lingering questions

Submit this response on Gradescope.

What lingering questions or thoughts do you have about this module?