Functors

In this first part of our series on monads, we aren't going to talk about monads at all! Instead we will talk about functors. Monads are a somewhat tricky idea. So to understand them better, it helps to start with functors, which are simpler in every way. If you already understand functors, you can move on to part 2 to learn about applicative functors.

What is a Functor?

A Functor is any type that can act as a generic container. A Functor allows us to transform the underlying values with a function, so that the values are all updated, but the structure of the container is the same. This is called "mapping".

How are Functors represented in Haskell?

Haskell represents the concept of a functor with the Functor typeclass. This typeclass has a single required function fmap.

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

We can see that fmap is a higher ordered function taking two inputs.

  1. A transformation function from an a type to a b type (a and b can be the same if we want)
  2. A functor containing values of type a

The output it produces is a new functor containing values of type b. This new functor has exactly the same structure (or shape) as the input functors; all that has changed is that each element has been modified by the input function.

Basic Functor Examples

A List is the most basic example of a functor. A list contains 0 or more elements of the same, underlying type. When we map a function over a list, we create a new list with the same number of elements where each item has been modified.

Let's see how this works in code.

>> let list1 = [] :: [String]
>> let list2 = [1, 2, 3] :: [Int]
>> let list3 = [True, False, True, False] :: [Bool]

For each of the underlying types above, we can think of a transformation function.

>> let f1 = length :: String -> Int
>> let f2 = (* 2) :: Int -> Int
>> let f3 = show :: Bool -> String

We can use fmap to apply each function to all elements of the input list. And the resulting list has the same "structure" (length), with each element corresponding to an element in the original list.

>> fmap f1 list1
[] :: [Int]
>> fmap f2 list2
[2, 4, 6] :: [Int]
>> fmap f3 list3
["True", "False", "True", "False"] :: [String]

Another simple example of a functor is the Maybe type. This object can contain a value of a particular type as Just, or it is Nothing (like a null value).

>> let maybe1 = Just "Hello" :: Maybe String
>> let maybe2 = Just 6 :: Maybe Int
>> let maybe3 = Nothing :: Maybe Bool

When we apply fmap, the "structure" of the object stays the same. With a list, this meant the size. With a Maybe, it means that if our input is Nothing, the output will be Nothing, and likewise with Just. Let's try using fmap with the same functions from above.

>> fmap f1 maybe1
Just 5 :: Maybe Int
>> fmap f2 maybe2
Just 12 :: Maybe Int
>> fmap f3 maybe3
Nothing :: Maybe String

How do I make a Functor instance?

You can make a Functor instance for any parameterized type that can act as a container. The only thing you really need to fill in is the fmap implementation. It will just look like this:

instance Functor MyType where
  fmap f (MyType ...) = ...

For a quick example, let's suppose we have a type describing the measurements of a home or apartment. We might want these measurements in different types or units, so we'll use a type parameter.

data Measurements a = Measurements
  { totalSize :: a
  , numBedrooms :: Int
  , masterBedroomSize :: a
  , livingRoomSize :: a
  } deriving (Show, Eq)

Now suppose we have a function to convert the measurement type. Here's all we need to do to implement the functor instance.

instance Functor Measurements where
  fmap convertMeasure (Measurements ts nb mbs lrs) = Measurements
    (convertMeasure ts) nb (convertMeasure mbs) (convertMeasure lrs)

Now we can define a number of conversion functions, for possible types we might store inside these measurements. We can have some newtype wrappers to encapsulate the units as well.

newtype SquareFeet = SquareFeet Double
  deriving (Show, Eq)

newtype SquareMeters = SquareMeters Double
  deriving (Show, Eq)

squareFeetToMeters :: SquareFeet -> SquareMeters
squareFeetToMeters (SquareFeet ft) = SquareMeters (ft / 10.764)

And now let's see our functor in action!

>> let m1 = Measurements 1200 3 200 700 :: Measurements Int
>> let m2 = fmap fromIntegral m1 :: Measurements Double
>> m2
Measurements 1200.0 3 200.0 700.0
>> let m3 = fmap SquareFeet m2
>> :t m3
Measurements SquareFeet
>> let m4 = squareFeetToMeters <$> m3
>> m4
Measurements (SquareMeters 111.48) 3 (SquareMeters 18.58) (SquareMeters 65.03)

First we convert our integers to floating point values with fromIntegral, then we wrap them in SquareFeet, and finally convert to SquareMeters. In the last example, we use the operator (<$>) which is equivalent tofmap!

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

What are the Functor Laws?

Functors have two laws: the identity law, and the composition law. These laws express behaviors that your functor instances should follow. If they don't, other programmers will be very confused at the effect your instances have on the program. Many structures have similar laws, including monads.

The identity law says that if you "map" the identity function (id) over your functor, the resulting functor should be the same. A succinct way of stating this is:

fmap id = id

The identity function shouldn't change underlying elements, so it shouldn't change the functor either!

The composition law says that if we "map" two functions in succession over our functor, this should be the same as "composing" the functions and simply mapping that one super-function.

(fmap f) . (fmap g) = fmap (f . g)

As an example, we can make a super-function of converting a double into SquareMeters by going through SquareFeet first:

doubleToSquareMeters :: Double -> SquareMeters
doubleToSquareMeters = SquareFeet . squareFeetToMeters

We can apply the functions separately (m1 -> m2 -> m3), or we can apply the super-function once (m4). The results (m3 and m4) are the same!

>> let m1 = Measurements 1200.0 3 200.0 700.0 :: Measurements Double
>> let m2 = fmap SquareFeet m1 :: Measurements SquareFeet
>> let m3 = fmap squareFeetToMeters m2
>> let m4 = fmap doubleToSquareMeters m1
>> m3 == m4
True

If this is confusing, don't worry. If you make your instances intuitively, they should almost certainly follow the laws! To break them, you would have to do something like this, introducing an arbitrary value into the instance:

fmap convertMeasure (Measurements ts nb mbs lrs) = Measurements
    0 nb (convertMeasure mbs) (convertMeasure lrs)

Now if we map the id function, we'll always get 0 for the total size, even if the input's total size is non-zero.

How do Functors Help with Monads?

Functors are an abstract mathematical structure that we represent in Haskell with a typeclass, and this structure has particular "laws" associated with it that dictate its expected behavior. Monads are the same way!

However, functors are simpler to understand. The functor typeclass has only one function and two straightforward laws. We can easily visualize what they do. Monads meanwhile have multiple functions and several more complicated laws.

Understanding abstract mathematical structures is a little tricky for most people. So it helps to start with a simpler idea like functors before we try to understand monads.

Conclusion

Hopefully this first part of our series has given you a better understanding of functors. To continue your journey of understanding monads and other functional structures, you can move on to part 2 where we'll talk about Applicative Functors!