Lecture 20 Functors

Joseph Haugh

University of New Mexico

Mappable Things

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.

Mapping Over Maybe

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)

Mapping Over Maybe

data Maybe a = Nothing | Just a

We are doing the same pattern we do for list map:

  • Nothing becomes Nothing
  • Just x becomes Just (f x)

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

Mapping Over Trees

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)

The Pattern

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:

  • Take a function f :: a -> b
  • Apply it to every a stored inside the structure
  • Produce the same structure, but now holding b values

Can we capture this pattern in a typeclass?

A Mappable 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.

Mappable: Lists

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]

Mappable: Maybe

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)

Mappable: Tree

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)

These Are Already Builtin

It turns out Haskell already has this typeclass:

  • Mappable is called Functor
  • fmap’ is called fmap

Functor

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

Type Constructors as Arguments to Functor

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.

Kinds: Types of Types

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.

Kinds: Types of Types

-- 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

Functor Requires Kind (* -> *)

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 :: *

Analogy: Partial Application of Functions

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 :: * -> *

Analogy: Partial Application of Types

-- 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.

Why Not instance Functor (Maybe Int)?

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.

The Right Way To Think About It

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.

Types With Two Type Arguments

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.

Functor for Either

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.

Functor for Either: In Action

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.

Functor for Tuples

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.

Functor for Tuples: In Action

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.

A Two-Parameter Tree

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)
  • k is the key type (used to organise the tree)
  • a is the value type stored at each leaf

Functor for Tree2

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”.

Functor for Tree2: In Action

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.

Summary: What Functor Requires

  • f in class Functor f must have kind (* -> *); it is a type constructor, not a concrete type
  • Concrete types (Int, Bool, Maybe Int) cannot be given to Functor
  • For types with one parameter (Maybe, [], Tree): instance is straightforward
  • For types with two parameters (Either, (,), Tree2): we partially apply the type constructor to get kind (* -> *)
  • The last type parameter is always the one that gets mapped over

The Functor Laws

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.

Functor Laws: Intuition

The laws ensure that fmap only touches the contents, never the structure:

  • fmap id = id: the shape of the container must not change
  • fmap (f . g) = fmap f . fmap g: one pass or two passes, same result

Our instances for [], Maybe, Tree, Either a, ((,) a), and Tree2 k all satisfy these laws.

Conclusion

  • Functor captures the idea of “things that can be mapped over”
  • The typeclass variable f has kind (* -> *); it is a type constructor, not a full type
  • We write instance Functor Maybe, not instance Functor (Maybe a), because the element type must remain free for fmap to change
  • For two-parameter types we partially apply the type constructor: Either a, ((,) a), Tree2 k: fixing the “context” and leaving the “contents” free
  • A lawful fmap preserves structure and only transforms values