Joseph Haugh
University of New Mexico
We have seen map for lists many times:
ghci> map (*2) [1, 2, 3, 4]
[2, 4, 6, 8]
ghci> map show [1, 2, 3]
["1", "2", "3"]
But lists are not the only type where “applying a function to the contents” makes sense.
Recall Maybe:
data Maybe a = Nothing | Just a
Suppose we have a Maybe Int and want to double the number if it exists:
doubleIfPresent :: Maybe Int -> Maybe Int
doubleIfPresent Nothing = Nothing
doubleIfPresent (Just n) = Just (n * 2)
data Maybe a = Nothing | Just a
We are doing the same pattern we do for list map:
Let’s write a general mapMaybe:
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing = Nothing
mapMaybe f (Just x) = Just (f x)
ghci> mapMaybe (*2) (Just 5)
Just 10
ghci> mapMaybe (*2) Nothing
Nothing
Recall our Tree type with values at the nodes:
data Tree a = Leaf | Node (Tree a) a (Tree a)
To apply a function to every value stored in the tree:
mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Leaf = Leaf
mapTree f (Node lt x rt) = Node (mapTree f lt) (f x) (mapTree f rt)
t :: Tree Int
t = Node (Node Leaf 2 Leaf) 5 (Node Leaf 7 Leaf)
ghci> mapTree (*10) t
Node (Node Leaf 20 Leaf) 50 (Node Leaf 70 Leaf)
We now have three separate map-like functions:
map :: (a -> b) -> [a] -> [b]
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapTree :: (a -> b) -> Tree a -> Tree b
The pattern is identical in all three:
Can we capture this pattern in a typeclass?
class Mappable f where
fmap' :: (a -> b) -> f a -> f b
Notice something new here: the type variable f is applied to a and b, so f is not a plain type like Int; it is a type constructor that takes one argument.
class Mappable f where
fmap' :: (a -> b) -> f a -> f b
instance Mappable [] where
fmap' = map
Note that we write [] not [a] in the instance head.
[] is the list type constructor; it takes a type argument and produces a concrete type:
[] Int = [Int]
[] Char = [Char] -- i.e. String
[] Bool = [Bool]
class Mappable f where
fmap' :: (a -> b) -> f a -> f b
instance Mappable Maybe where
fmap' f Nothing = Nothing
fmap' f (Just x) = Just (f x)
class Mappable f where
fmap' :: (a -> b) -> f a -> f b
instance Mappable Tree where
fmap' f Leaf = Leaf
fmap' f (Node lt x rt) = Node (fmap' f lt) (f x) (fmap' f rt)
It turns out Haskell already has this typeclass:
class Functor f where
fmap :: (a -> b) -> f a -> f b
instance Functor [] where
fmap = map
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
ghci> fmap (*2) [1,2,3]
[2,4,6]
ghci> fmap (*2) (Just 5)
Just 10
ghci> fmap (*2) Nothing
Nothing
Look again at the typeclass definition:
class Functor f where
fmap :: (a -> b) -> f a -> f b
The variable f appears applied to a type: f a, f b.
This means f cannot be a concrete type like Int or Bool; those cannot be applied to another type.
f must be a type constructor: something that takes one type argument to produce a concrete type.
Haskell uses kinds to describe the “shape” of types:
-- Concrete types have kind *
Int :: *
Bool :: *
Char :: *
-- Type constructors have kind (* -> *)
Maybe :: * -> *
[] :: * -> *
Tree :: * -> *
A kind is to types what a type is to values.
-- Concrete types have kind *
Int :: *
Bool :: *
Char :: *
-- Type constructors have kind (* -> *)
Maybe :: * -> *
[] :: * -> *
Tree :: * -> *
Just as f :: Int -> Bool takes an Int value and returns a Bool value, Maybe :: * -> * takes a kind-* type and returns a kind-* type:
Maybe Int :: * -- concrete type
Maybe Bool :: * -- concrete type
Maybe :: * -> * -- still waiting for an argument
The variable f in Functor must have kind ( -> ):
class Functor f where -- f :: * -> *
fmap :: (a -> b) -> f a -> f b
This is why we write:
instance Functor Maybe where ... -- OK: Maybe :: * -> *
instance Functor [] where ... -- OK: [] :: * -> *
instance Functor Tree where ... -- OK: Tree :: * -> *
But NOT:
instance Functor Int where ... -- ERROR: Int :: *
instance Functor (Maybe Int) where ... -- ERROR: Maybe Int :: *
Recall partial application of functions:
add :: Int -> Int -> Int
add x y = x + y
add3 :: Int -> Int
add3 = add 3 -- partially applied
Type constructors work the same way:
-- Either takes TWO type arguments
Either :: * -> * -> *
-- Partially applying Either gives kind (* -> *)
Either String :: * -> *
-- Full application gives a concrete type
Either String Int :: * -- Left String or Right Int
-- One argument applied -- still a type constructor
Either String :: * -> *
So Either String can be given to Functor!
instance Functor (Either String) where ...
We will return to this shortly.
Let’s see what would happen:
-- What the instance would look like:
instance Functor (Maybe Int) where
fmap :: (a -> b) -> Maybe Int a -> Maybe Int b
-- ^^^^^^^^^^^
-- This does not make sense!
-- Maybe Int is already concrete (kind *)
-- It cannot be applied to another type
Maybe Int holds Int values. There are no “contents” left to map over; the element type is already fixed.
When we write instance Functor Maybe, we are saying:
For any element type a, the structure Maybe a can be mapped over.
The instance fixes the container (Maybe) and leaves the element type free:
fmap :: (a -> b) -> Maybe a -> Maybe b
-- ^^^^^^^ ^^^^^^^
-- container container
-- with a's with b's
If we fixed both (writing Functor (Maybe Int)), there would be nothing left for fmap to change.
What happens when a type has two type parameters?
data Either a b = Left a | Right b
data (,) a b = (a, b)
These have kind (* -> * -> *); they need two type arguments to become concrete.
Functor requires kind (* -> *); it needs exactly one more argument.
So we must partially apply these type constructors before we can write a Functor instance.
data Either a b = Left a | Right b
The natural choice: fmap applies the function to a Right value and leaves Left untouched:
instance Functor (Either a) where
fmap f (Left x) = Left x
fmap f (Right y) = Right (f y)
Notice the instance head is Either a, not Either a b. The element type b is what gets mapped over.
instance Functor (Either a) where
fmap f (Left x) = Left x
fmap f (Right y) = Right (f y)
ghci> fmap (*2) (Right 5)
Right 10
ghci> fmap (*2) (Left "error")
Left "error"
Left is often used to carry an error message, so it makes sense that fmap skips it: a failed computation stays failed.
data (,) a b = (a, b)
The natural choice: fmap applies the function to the second component and leaves the first alone:
instance Functor ((,) a) where
fmap f (x, y) = (x, f y)
Again the instance head is ((,) a), partially applying the tuple constructor to fix the type of the first component.
instance Functor ((,) a) where
fmap f (x, y) = (x, f y)
ghci> fmap (*2) ("hello", 5)
("hello", 10)
ghci> fmap show ("hello", 42)
("hello", "42")
The first component (of type a) is left alone; it is the “context” or “tag”.
The second component (of type b) is mapped over.
Let’s define a tree that stores keys at nodes and values at leaves:
data Tree2 k a = Leaf2 a | Node2 (Tree2 k a) k (Tree2 k a)
data Tree2 k a = Leaf2 a | Node2 (Tree2 k a) k (Tree2 k a)
instance Functor (Tree2 k) where
fmap f (Leaf2 x) = Leaf2 (f x)
fmap f (Node2 lt k rt) = Node2 (fmap f lt) k (fmap f rt)
fmap reaches into every Leaf2 and transforms the value with f.
The key k at each internal node is left untouched; it is the “container structure”, not the “contents”.
data Tree2 k a = Leaf2 a | Node2 (Tree2 k a) k (Tree2 k a)
instance Functor (Tree2 k) where
fmap f (Leaf2 x) = Leaf2 (f x)
fmap f (Node2 lt k rt) = Node2 (fmap f lt) k (fmap f rt)
t :: Tree2 String Int
t = Node2 (Leaf2 1) "left" (Node2 (Leaf2 2) "right" (Leaf2 3))
ghci> fmap (*10) t
Node2 (Leaf2 10) "left" (Node2 (Leaf2 20) "right" (Leaf2 30))
The String keys are unchanged. Only the Int values are scaled.
A lawful Functor instance must satisfy two laws:
Identity law:
fmap id = id
Mapping the identity function does nothing.
Composition law:
fmap (f . g) = fmap f . fmap g
Mapping a composed function is the same as mapping each function separately.
The laws ensure that fmap only touches the contents, never the structure:
Our instances for [], Maybe, Tree, Either a, ((,) a), and Tree2 k all satisfy these laws.