Road to Haskeller #13 - Functor

Last Edited: 6/26/2024

The blog post introduces an important typeclass, Functor, in Haskell.

Haskell & Functor

Functor Typeclass

One of the most important concepts in Haskell that many get confused about is Monad. It is hard because many rush too much and try to understand what it is as quickly as possible. Here, I will take a different approach and go over prerequisites slowly but surely for us to truly understand Monad. One of the prerequisites is Functor. Functor is a typeclass for type constructors with exactly 1 type that can be mapped over. A type constructor is a function that takes 1 or more types as parameters to create a concrete type. The following is how we can create an instance of Functor for a type constructor f.

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

To make a type constructor a valid instance of the Functor typeclass, we just need to define fmap, which can take a function (a -> b) and apply it to the value of a type inside the type constructor. This might be hard to understand without a concrete example, so let's see some examples.

Lists

A list is a type constructor that takes a type like Int, Float, or String and generates a new type [Int], [Float], and [String], which is also a functor or a valid instance of Functor. In fact, map is just a fmap that works only on lists. Here's how the list is an instance of the Functor typeclass.

instance Functor [] where
    fmap = map
 
map :: (a -> b) -> [a] -> [b]

That's it. The map function takes a function and applies it to the elements in a list, which is the same thing as fmap. Thus, we can do the following.

ghci> map (*2) [1..3]
[2,4,6]
ghci> fmap (*2) [1..3]
[2,4,6]
ghci> map (++"!") ["Hello", "Nice to meet you"]
["Hello!","Nice to meet you!"]
ghci> fmap (++"!") ["Hello", "Nice to meet you"]
["Hello!","Nice to meet you!"]

Maybe

Another example of a type constructor/functor is Maybe, which takes a type like Int, Float, or String and generates a new type Maybe Int, Maybe Float, and Maybe String. Here is how Maybe is an instance of the Functor typeclass

instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

We are using pattern matching to define fmap for different value constructors Just and Nothing. As Nothing has null, we want Nothing as an output, while we want to apply a function f to a value inside a Just. This means we can use the fmap function on Maybe to perform the following.

ghci> fmap (*2) (Just 4)
Just 8
ghci> fmap (*2) Nothing
Nothing
ghci> fmap (++"!") (Just "Hello")
"Hello!"
ghci> fmap (++"!") Nothing
Nothing

IO

IO is actually a type constructor/functor as well (IO (), IO String, IO Float, etc.) that has a corresponding fmap function.

instance Functor IO where
    fmap f action = do
        result <- action
        return (f result)

It binds a value out of an IO action to result, applies the function f to result, and creates an IO action with that value using return. Hence, we can do something like the following.

ghci> fmap (++"!") getLine
Hello
"Hello!"

ADVANCED CHALLENGE: (->) r is also a valid instance of Functor. Try coming up with how its fmap is defined. (Hint: -> is used in type definition like r -> a, and it is also a type constructor that take two types as parameters to create a new type for a function. )

Exercises

This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources