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
classandinstancedeclarations, - read the
Functortypeclass, - implement
fmapfor a binary tree, - use
fmapwithMaybe, 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:
- What does mapping change, and what does it preserve?
- What does the type of
fmapsay, and why is the instance forTreerather thanTree Int? - How does
fmapbehave onJustandNothing? - What does the identity law
fmap id = idrequire?
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?