Functor, Applicative, and Why

Leo Zhang
Axiom Zen Team
Published in
15 min readJul 12, 2018

This blog post will explain two core concepts in Haskell — Functor and Applicative, which also exist in many other functional languages. Functor and Applicative are great abstractions that allow us to reuse lots of code.

However, when I was learning these concepts, it was difficult for me. Not just because they’re abstract, but because most of the articles and posts I read about Functor and Applicative start by introducing their definition and then give examples.

I feel it should be the other way around. It’s easier to learn abstract things by seeing concrete instances or use cases of them.

So in this post, I’m starting with concrete examples then tying them back to the definitions of Functor and Applicative.

Input validation

Let’s say we have a greet function in Javascript that takes a user object and returns a string:

var greet = function(user) {
return "Hello " + user.name;
};

If we try it in the node REPL, it should work.

> greet({ name: "Alice" })
'Hello Alice'

However, if we enter an undefined value, it will throw an exception.

> greet(undefined)
TypeError: Cannot read property 'name' of undefined
...

Why would the user be undefined?

Well, Javascript is an untyped language, so any variable could be undefined. This function assumes the input is not undefined. But it's common for developers to forget about this assumption.

In order to handle the undefined input, we could wrap this function with a function that validates the input:

var checkAndGreet = function(user) {
if (!user) {
return undefined;
}
return greet(user);
};

So if the user is undefined, we wouldn't pass it to greet, but short circuit to return undefined.

Maybe type in Haskell

Let’s see how this case is handled in Haskell.

First, let’s define the User type and the greet function.

data User = User String 
deriving (Show)
greet :: User -> String
greet (User name) = "Hello " ++ name

Test it in GHCi:

> User "Alice"
User "Alice"
> greet (User "Alice")
"Hello Alice"

And let’s visualize the function call like this:

In Haskell, this case is handled by a data type called Maybe. This is the type declaration of Maybe, which says the generic Maybe type is either a Just value that contains other values, or a constant Nothing value.

data Maybe a = Nothing | Just a 
deriving (Eq, Ord)

So Maybe User is a type that can present two cases, either Nothing or a Just User.

If the input is a Maybe User, then we can make a checkAndGreet to take that and return a Maybe String.

checkAndGreet :: Maybe User -> Maybe String
checkAndGreet Nothing = Nothing
checkAndGreet (Just user) = Just (greet user)

The first line defines the Maybe

Let’s try it in GHCi

> checkAndGreet Nothing
Nothing
> user = User "Alice"> checkAndGreet (Just user)
Just "Hello Alice"

How does it prevent mistakes?

Where would we get a Maybe User?

Well, let’s say we don’t want the user name to be empty, so we can create a validateAndMakeUser function to check if the name is empty and returns a Maybe User.

validateAndMakeUser :: String -> Maybe User
validateAndMakeUser "" = Nothing
validateAndMakeUser name = Just (User name)

Let’s look at the mistake we had before.

If we have a user name of String and we forget to validate the name and pass it directly to greet, the Haskell compiler won't allow it. The compiler will say greet takes a User, which is not a String.

> greet "Alice"
error:
• Couldn't match expected type ‘User’ with actual type ‘[Char]’
• In the first argument of ‘greet’, namely ‘"Alice"’
In the expression: greet "Alice"
In an equation for ‘it’: it = greet "Alice"

If we remember to validate the name, and now we have a Maybe User value, but still we passed it to greet instead of checkAndGreet, then the code won't compile either. Because Maybe User and User are different types.

> greet (Just (User "Alice"))<interactive>:20:8: error:
• Couldn't match expected type ‘User’ with actual type ‘Maybe User’
• In the first argument of ‘greet’, namely ‘(Just (User "Alice"))’
In the expression: greet (Just (User "Alice"))
In an equation for ‘it’: it = greet (Just (User "Alice"))

Extract the input validation part into a function

Alright. Let’s say we have other functions that take User and returns a different String, for instance, a bye function:

bye :: User -> String
bye (User name) = "Goodbye " ++ name

However, the input value we have is a Maybe User value, not a User. How can I take the User from the Maybe User and pass it to the bye function?

Then we can wrap it the same way as checkAndGreet to make a checkAndBye.

checkAndBye :: Maybe User -> Maybe String
checkAndBye Nothing = Nothing
checkAndBye (Just user) = Just (bye user)

As you probably noticed, the checkAndGreet and checkAndBye are very similar. We're kind of repeating the logic here.

We could extract the common part into a function, and use that to make checkAndGreet and checkAndBye. Let's name this common function mapUser.

mapUser :: (User -> String) -> Maybe User -> Maybe String
mapUser f Nothing = Nothing
mapUser f (Just user) = Just (f user)
checkAndGreet :: Maybe User -> Maybe String
checkAndGreet maybeUser = mapUser greet maybeUser
checkAndBye :: Maybe User -> Maybe String
checkAndBye maybeUser = mapUser bye maybeUser

We can further refactor them into point-free style:

checkAndGreet :: Maybe User -> Maybe String
checkAndGreet = mapUser greet
checkAndBye :: Maybe User -> Maybe String
checkAndBye = mapUser bye

With the generic mapUser, you can get checkAndGreet and checkAndBye for free.

Generalize the mapUser function

Let’s take a look at the mapUser function again. Even though this function is called mapUser, it didn't actually use anything special about User, nor about String

mapUser :: (User -> String) -> Maybe User -> Maybe String
mapUser f Nothing = Nothing
mapUser f (Just user) = Just (f user)

If we replace User with a generic type a and replace String with a generic type b, then mapUser is equivalent to the mapMaybe function as below:

mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing = Nothing
mapMaybe f (Just a) = Just (f a)

Then we can refactor the checkAndGreet and checkAndBye with the mapMaybe function, and it will still type-check and work.

checkAndGreet :: Maybe User -> Maybe String
checkAndGreet = mapMaybe greet
checkAndBye :: Maybe User -> Maybe String
checkAndBye = mapMaybe bye

Now we have a generic mapMaybe function that can deal with a case where the input is empty (Nothing).

With Haskell’s strong type system, we can just write functions that process on concrete types, and if we need those functions to process the Maybe values we get them from DB calls or HTTP request, we can now just wrap the function with mapMaybe to get a new function that is able to process over Maybe values.

The mapMaybe function can be implemented in Javascript too. Given there is no Maybe type, we would just call it mapNullable:

var mapNullable = function(f) {
return function(v) {
if (v === null || v === undefined) {
return null;
}
return f(v);
};
};
var greet = function(user) {
return "Hello " + user.name;
};
var bye = function(user) {
return "Bye " + user.name;
};
var checkAndGreet = mapNullable(greet);
var checkAndBye = mapNullable(bye);
console.log(checkAndGreet({ name: 'Alice' })); // "Hello Alice"
console.log(checkAndBye(undefined)); // undefined

Map over 2 Maybe values

Let’s back to our Haskell version. We have a generic mapMaybe to map a function over a Maybe value. But it seems to only work for functions that take just one argument.

mapMaybe :: (a -> b) -> Maybe a -> Maybe b

What if my function takes more than one argument? Is it possible to have a function that maps over multiple Maybe values? For example, map2Maybes and map3Maybes with the following type signatures:

map2Maybes :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe cmap3Maybes :: (a -> b -> c -> d) 
-> Maybe a
-> Maybe b
-> Maybe c
-> Maybe d

And let’s call a function that takes N arguments an "N-arity" function. So mapMaybe is 1-arity function, and map2Maybes is 2-arity function, map3Maybes is 3-arity function.

If we have the map2Maybes function, then we can use it to wrap a function that takes two Maybe User values as input.

showParents :: User -> User -> String
showParents (User fatherName) (User motherName) =
"Father is " ++ fatherName ++ " and mother is " ++ motherName
checkAndShowParents :: Maybe User -> Maybe User -> Maybe String
checkAndShowParents maybeFather maybeMother =
map2Maybes showParents maybeFather maybeMother

And the map2Maybes is not hard to implement:

map2Maybes :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
map2Maybes _ Nothing _ = Nothing
map2Maybes _ _ Nothing = Nothing
map2Maybes f (Just a) (Just b) = Just (f a b)

Similarly, you can implement map3Maybes, map4Maybes ... We can make as many as we want. However, we still need to write them manually each time for wrapping an N-arity function.

Is it possible to have a generic mapNMaybes function that works for functions that take any number of arguments?

Let’s find out.

Map over N Maybe values

In Haskell, every function is curried. We can pass a value to a 2-arity function to get a new 1-arity function.

> :t showParents (User “Bob”)
showParents (User “Bob”) :: User -> String

Therefore, showParents's type signature could also be written as:

showParents :: User -> (User -> String)

If we treat the 1-arity function (User -> String) as a value, then showParents can be passed to mapMaybe and it will return a Maybe (User -> String) type

> :t mapMaybe showParents (Just (User “Bob”))
mapMaybe showParents (Just (User “Bob”)) :: Maybe (User -> String)

But wait a second, can a Maybe type contain a function?

Yes, why not? Maybe a is a generic type, and it can take any concrete type to make a new type. Since a function is also a concrete type, it can be wrapped in a Maybe value too. And it doesn't matter how many arguments it takes.

For instance, we can just pass any function to one of the Maybe type constructor Just:

> :t Just greet
Just greet :: Maybe (User -> String)
> :t Just showParents
Just showParents :: Maybe (User -> User -> String)

But what can we do with a Maybe (User -> String) value?

Well, let’s take a look at what we need. We’d like to implement a checkAndShowParents function.

checkAndShowParents :: (User -> User -> String) 
-> Maybe User
-> Maybe User
-> Maybe String

Since we’ve got

mapMaybe :: (User -> (User -> String)) 
-> Maybe User
-> Maybe (User -> String)

Or

mapMaybe :: (User -> User -> String) 
-> Maybe User
-> Maybe (User -> String)

If we have another function with the following type signature, let’s call it applyShowParents for now.

applyShowParents :: Maybe (User -> String) 
-> Maybe User
-> Maybe String

Then we can composite it with the mapMaybe to make checkAndShowParents:

checkAndShowParents :: Maybe User -> Maybe User -> Maybe String
checkAndShowParents maybeFather maybeMother =
applyShowParents (mapMaybe showParents maybeFather) maybeMother
applyShowParents :: Maybe (User -> String)
-> Maybe User
-> Maybe String
applyShowParents Nothing _ = Nothing
applyShowParents _ Nothing = Nothing
applyShowParents (Just f) (Just user) =
Just (f user)Generalize to applyMaybe

Again, we can generalize the applyShowParents as well into a generic applyMaybe function, because the function body doesn't need anything special from either User or String.

applyMaybe :: Maybe (a -> b) -> Maybe a -> Maybe b
applyMaybe Nothing _ = Nothing
applyMaybe _ Nothing = Nothing
applyMaybe (Just f) (Just a) = Just (f a)

And the checkAndShowParents can be implemented by compositing mapMaybe, applyMaybe and showParents

checkAndShowParents :: Maybe User -> Maybe User -> Maybe String
checkAndShowParents maybeFather maybeMother = applyMaybe
(mapMaybe showParents maybeFather) maybeMother

Generalize into N-arity function

Now that we can map over 2-arity functions, can it apply to N-arity functions?

Yes. Observe the applyMaybe's type signature and how we can use it for a 3-arity functions or N-arity too:

applyMaybe :: Maybe (a -> b -> c) -> Maybe a -> Maybe (b -> c)applyMaybe :: Maybe (a -> b -> c -> d) 
-> Maybe a
-> Maybe (b -> c -> d)
applyMaybe :: Maybe (a -> b -> c -> d -> e)
-> Maybe a
-> Maybe (b -> c -> d -> e)
...

And with the following functions we can reduce a Maybe N-arity function value into a Maybe (N-1)-arity function value, which can be further reduced all the way to a Maybe value.

> add3 a b c = a + b + c + (1 :: Int)> :t add3
add3 :: Int -> Int -> Int -> Int
> :t applyMaybe (Just add3) (Just 1)
applyMaybe (Just add3) (Just 1) :: Maybe Int -> Maybe (Int -> Int -> Int)
> :t applyMaybe (applyMaybe (Just add3) (Just 1)) (Just 2)
:t applyMaybe (Just add3) (Just 1) :: Maybe Int -> Maybe (Int -> Int)
> :t applyMaybe (applyMaybe (applyMaybe (Just add3) (Just 1)) (Just 2)) (Just3)
:t applyMaybe (applyMaybe (applyMaybe (Just add3) (Just 1)) (Just 2)) (Just3) :: Maybe Int

The above expression looks a bit messy. Let’s rewrite it as infix operator.

> Just add3 `applyMaybe` (Just 1) `applyMaybe` (Just 2) `applyMaybe` (Just 3)
Just 7
> Just add3 `applyMaybe` Nothing `applyMaybe` (Just 2) `applyMaybe` (Just 3)
Nothing

Pattern of writing input validation for functions

We’ve made a generic mapMaybe function and an applyMaybe function that can be used to wrap any N-arity functions. That way, they're able to take values from N Maybe and shortcircuit to return Nothing if any of the N Maybe values are Nothing.

greet :: User -> String
greet (User name) = "Hello " ++ name
checkAndGreet :: Maybe User -> Maybe String
checkAndGreet = mapMaybe
showParents :: User -> User -> String
showParents (User fatherName) (User motherName) =
"Father is " ++ fatherName ++ " and mother is " ++ motherName
checkAndShowParents :: Maybe User -> Maybe User -> Maybe String
checkAndShowParents maybeFather maybeMother =
showParents `mapMaybe` maybeFather `applyMaybe` maybeMother
add3 :: Int -> Int -> Int -> Int
add3 a b c = a + b + c + 1
checkAndAdd3 :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
checkAndAdd3 ma mb mc = add3 `mapMaybe` ma
`applyMaybe` mb
`applyMaybe` mc

You might notice a pattern here: if we want to wrap an N-arity function with empty input checks and the function has only 1 argument, we can just use mapMaybe. If there is more than 1 argument, we just append (applyMaybe argN) in the end.

Functor and Applicative

OK, with the above examples and practices in mind, now it’s time to introduce the term Functor and Applicative.

What is Functor? Functor is a typeclass that essentially defines a list of functions to implement.

Functor typeclass defines just one function fmap with the following type signature.

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

Recall the mapMaybe function we created earlier. It's the fmap function for Maybe to be a Functor. In other words, Maybe is a Functor because Maybe implements fmap:

mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing = Nothing
mapMaybe f (Just a) = Just (f a)
instance Functor Maybe where
fmap = mapMaybe

fmap is also defined as the inflx operator <$>. Therefore, the following two operations are identical:

infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
fmap greet (Just (User "Alice"))
greet <$> Just (User "Alice")

What is Applicative?

Applicative is also a typeclass. To be an Applicative, the type has also to be a Functor.

Applicative defines two functions, pure and <*>, with the following type signature:

class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

Maybe is an instance of Applicative. Refer to the type signature of Just and applyMaybe: these two functions are Maybe type's implementation for being Applicative.

Just :: a -> Maybe aapplyMaybe :: Maybe (a -> b) -> Maybe a -> Maybe bclass Applicative Maybe where
pure = Just
(<*>) = applyMaybe

The abstraction of Functor and Applicative allows more generic functions to be built and reused. For example, the liftA2 and liftA3 are generic versions of the map2Maybes and map3Maybes for Applicative.

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cliftA3 :: Applicative f => (a -> b -> c -> d) 
-> f a
-> f b
-> f c
-> f d

Both liftA2 and liftA3 can be implemented with just pure and <*>. You can try to implement them yourself.

More Functor examples

So far we’ve seen an instance of Functor and Applicative, which is Maybe. Actually, there are a lot more of them defined in the base module, and other modules.

The most useful Applicatives (which also means they are Functors) are Maybe, Either, IO and List, which means you can use the same functions fmap, (<*>) on all those types.

For instance, IO is also a Functor and an Applicative. Here is an example of how to read and parse environment variables with the functions provided by Functor and Applicative.

To read the environment variables by name, there is a getEnv function under the namespace System.Environment that takes String as the env var name and returns the env var as IO String.

> import System.Environment (getEnv)
> :t getEnv
getEnv :: String -> IO String

IO String is a computation that might run into an exception. For instance,

> getEnv "NAME"
*** Exception: NAME: getEnv: does not exist (no environment variable)
> ("NAME: " ++) <$> getEnv "NAME"
*** Exception: NAME: getEnv: does not exist (no environment variable)
> ("PORT: " ++) <$> getEnv "PORT"
"4567"

And we can make a readConfig function by reusing the <$> and <*> function like this:

data Config = Config
{ cfgHost :: String
, cfgPort :: Int
, cfgDebug :: Bool
} deriving (Show)
readConfig :: IO Config
readConfig = Config <$> getHost
<*> getPort
<*> getDebug
getHost :: IO String
getHost = getEnv "HOST"
getPort :: IO Int
getPort = read <$> getEnv "PORT"
getDebug :: IO Bool
getDebug = read <$> getEnv "DEBUG"

Notice that the read function is a polymorphic parse function that can be either String -> Int or String -> Bool depending on where it's used.

So if the environment doesn’t have HOST, then the IO will throw an exception:

> readConfig
*** Exception: HOST: getEnv: does not exist (no environment variable)

If all the environment variables are present, we’ll get a Config value with all the parsed value in it.

> readConfig
Config {cfgHost = "localhost", cfgPort = 4567, cfgDebug = True}

Why?

To summarize my points with a few QnAs.

So what is Functor and Applicative?

They are typeclasses (like interfaces in other languages) that define the functions that their type instances have to implement.

Why do I need Functor?

Because we want to reuse code.

Functor generalizes how to map a function from one value to another. We used the Maybe type as an example to show why and how to use a generic mapMaybe function without having to deal with the empty case. And the mapMaybe is the implementation of Functor for Maybe type.

Why do I need Applicative?

Because Functor can only map a function which takes one argument. If we have a function that takes multiple arguments, we need Applicative.

Applicative provides abstraction for how to apply a function that takes multiple arguments over multiple values. We used applyMaybe, which is the implementation of Applicative for Maybe type, as an example to show how to map a function over multiple Maybe values without dealing with the case of any value being Nothing.

Why do I need Functor and Applicative instead of just mapMaybe and applyMaybe?

There are more instances of Functor and Applicative. For example, Maybe, Either, IO, List are all Functor and Applicative. You can reuse the same fmap and <*> function to map functions over those values without having to write mapMaybe, mapEither, mapList, etc.

And functions like liftA2, which is defined on top of Functor and Applicative, can also be reused for free without having to write may2Maybes, map2IOs, map2Eithers etc.

Conclusion

Functor and Applicative are two key concepts in functional programming. We used the Maybe type and its use cases as examples to introduce the need for abstraction. Functor and Applicative are abstractions; they define what functions have to be implemented for a type to be an instance of them.

Functional programming has plenty more abstractions and type classes built on top of Functor and Applicative. This makes a great amount of code and logic reusable, and Haskell's strong type system ensures that the use of those generic functions are correct and safe.

Thanks for reading this blog post. Hopefully, I made Functor and Applicative a bit easier to understand.

--

--