Your easy guide to Monads, Applicatives, & Functors

Are you curious about monads? 🤔 Or maybe you’re further down the rabbit 🐰 hole, looking into applicatives? Does “functor” sound alien 👽 to you?

No worries! 😃

Monad, applicative functor, and functor are just functional programming patterns you can use to deal with effects like lists/arrays, trees, hashes/dictionaries, and even functions.

Functor is the simplest pattern, so it makes sense to start there. As you work your way to monad, you’ll see that functor is the basis for applicative functor which is the basis for monad.

⚠️ If you don’t get the animations, don’t worry. Just focus on the code samples and explanations. The animations are there for some visual intuition. 👍

What’s a functor?

A functor is a list.

Created with Blender and Gifcurry.
map(function)(list) = [function(element) | element <- list]
add1(input) = input + 1
map(add1)([1,2,3])
-- [2,3,4]

The list functor takes a function and applies it to every element inside the list. The function doesn’t have to consider or worry about the list — map handles the list.

A functor is a function.

Created with Blender and Gifcurry.
map(function1)(function2)(input) = function1(function2(input))

sub2(input) = input - 2
map(add1)(sub2)(1)
-- 0

The function functor composes or glues two functions together returning a new function. This new function routes its input through the two functions returning the output of the first function.

A functor is a promise.

Created with Blender and Gifcurry.
map(function)(promise) = fmap(function)(promise)
promise <- async(return 11)
wait(map(sub2)(promise))
-- 9

The promise functor applies a function to its fulfilled input and returns a new promise that will contain the output of the function.

A functor is an effect that you lawfully define map for.

Created with Blender and Gifcurry.
map
:: (input -> output) -- Takes a function.
-> effect(input ) -- Takes input inside an effect.
-> effect( output) -- Returns output inside an effect.

In general, a functor is just map defined for some effect. The map function

  • takes a function input -> output ,
  • takes input for that function stuck inside an effect effect(input) ,
  • passes the input to the function,
  • collects the function’s output,
  • and returns the output stuck inside the effect effect(output) .

Wait, what do you mean by “lawfully define?”

The map definition must obey the functor laws.

How many functor laws are there?

There are two laws your map definition must obey.

Okay, what’s the first law?

If you pass identity to map, map’s output must equal its input.

Created with Blender and Gifcurry.
list = [1,2,3]
identity(input) = input
list == map(identity)(list)
-- True

Hmm, what’s the second and last law?

If you compose two or more functions and pass that to map, map’s output must equal the output of composing multiple calls of map together— one for each function.

Created with Blender and Gifcurry.
times3(input) = input * 3
composition(input) = add1(sub2(times3(input)))
composition(1)
-- 2
list = [1,2,3]
map(composition)(list) == map(add1)(map(sub2)(map(times3)(list)))
-- True

One call to map using composition and list equaled three calls to map using add1, sub2, and times3 and list.

So functor is an interface?

Yes. If you define a lawful version of map for some effect, it is an instance of functor or mappable. map is an overloaded function that is defined differently for each type of effect you define it for.

If you’ve used the usual map for lists/arrays— found in most standard libraries — you’ve been using functors all along. 😺

What do you mean by effect?

List/Array is an effect. Tuple/Struct/Record is an effect. Future/Promise is an effect. Function is an effect. Tree is an effect. HashMap/Hash/Dictionary is an effect. Maybe/Optional/Nullable is an effect. Either (or the more specialized try) is an effect. There are many effects. These are some of them.

Ask what an effect is and you’ll see or hear “context”, “functorial context”, “container”, “container and payload”, “wrapper”, “box”, “computation”, “ambiguous computation”, “structures”, “data structures”, “nondeterminism”, “nondeterministic”, “type constructors”, “side-effects”, “a side-effect made explicit”, “impure”, and/or “not pure”. Phew.

The definition of effect is “power to bring about a result”. This is true for anything I’ve seen called an effect. They have the power or capability to bring about a result but may not always. For example, the empty list/array or nothing/none/null for maybe/optional/nullable.

I still don’t get functor.

A functor lifts or upgrades a function from one that cannot operate on an effect to one that can work on a single effect, leaving the effect intact after the function is done.

In terms of an interface, if you want some effect to be a functor, you have to (lawfully) define map for it.

Can map accept a function with more than one parameter?

If some function takes more than one parameter and you map it over some functor, it will be partially applied and stuck in the effect.

Take lists for example.

add(left)(right) = left + right
map(add)([1,2,3])
-- [ add(1),
-- add(2),
-- add(3) ]

add was partially applied to 1, 2, and 3 making for three partial applications of add stuck inside the list. Partially applied meaning the function is waiting on its other parameters. add(1) for example is waiting on one more number.

[ add(1),
add(2),
add(3) ]

⚠️ At this point you’re stuck. You can’t feed this result back into map in order to supply more input to the adds. map doesn’t take partially applied functions — waiting inside an effect/list — so you’ll need applicative functor to work with this result.

What are applicative functors?

An applicative functor is a list.

Created with Blender and Gifcurry.
pure(input) = [input]
apply(functions)(list) =
[ element | function <- functions,
element <- map(function)(list)
]
apply(pure(add1))([1,2,3])
-- [2,3,4]
apply(apply(pure(add))([1,2,3]))([4,5,6])
-- [5,6,7,6,7,8,7,8,9]
apply(map(add)([1,2,3]))([4,5,6])        
-- [5,6,7,6,7,8,7,8,9]

It returns [5, 6, 7, 6, 7, 8, 7, 8, 9] because it adds 1 to 4, 5, and 6, adds 2 to 4, 5, and 6, and then adds 3 to 4, 5, and 6.

💡 Instead of thinking about a list as a container for values, think of it as a nondeterministic value or put another way, a choice.

The list applicative functor computes all of the possible combinations or choices when sequencing or composing lists. You can represent this as a tree.

Created with Blender and Gifcurry.

Anyway, notice how I had to use one apply for every parameter add takes. One for the first parameter and another one for the second parameter.

The pure and first apply call wrapped three partially applied adds inside a list but you can substitute the pure and one apply call with map as using add with map also wraps three partially applied adds inside a list. We saw this up above with functor.

apply(pure(add))([1,2,3])
-- [ add(1),
-- add(2),
-- add(3) ]
map(add)([1,2,3])
-- [ add(1),
-- add(2),
-- add(3) ]

🤔 So far all of the examples had non-empty lists but what happens if any one list is empty?

[   ] `apply` [1,2,3] `apply` [4,5,6]
-- []
[add] `apply` [ ] `apply` [4,5,6]
-- []
[add] `apply` [1,2,3] `apply` [ ]
-- []

You get an empty list. Tracing the implementation, you can see that if any one list is empty, it propagates out. Doesn’t matter what the other lists had, you’ll always get an empty list if any one of the lists are empty.

If any of the effects are empty or failed, for the list effect and others (like maybe or either), the first occurrence will spill out in the end. This works out nicely as you don’t have to consider the empty case when applying lists or even worse, experience a crash because you ended up with an empty list somewhere.

💡One of the big aspects of applicative functor is that the effects play out before the lifted/upgraded function can do its thing. Applicative functor is like the Ronco — set it and forget it — showtime rotisserie 🍗 of functional programming patterns. Once we’ve kicked off the process, we can only wait for the end result.

There’s no picking and choosing what’s next based on what’s already occurred. We don’t get to look at an intermediate result and make a decision. It’s been said that applicative functor is batch versus interactive processing as is the case with monads.

Let’s say we have a function called if_x_then_y_else_z (example adapted from Applicative Programming with Effects, Conor McBride and Ross Paterson. in Journal of Functional Programming 18:1 (2008), pages 1–13).

if_a_then_b_else_c(a)(b)(c) = if a then b else c
pure(if_a_then_b_else_c) `apply` x `apply` y `apply` z

It takes in three effects and depending on x’s result, it either picks the result from y or z. Since we’re talking about lists at this point, let’s use the list applicative functor.

if_x_then_y_else_z([True])(["y result"])(["z result"])
-- ["y result"]
if_x_then_y_else_z([False])(["y result"])(["z result"])
-- ["z result"]

So if an item of x is True, we go with y’s item else we go with z’s.

if_x_then_y_else_z([True])(["y result"])(["z result"])
-- ["y result"]

For the inputs

  • [True]
  • ["y result"]
  • and ["z result"]

it doesn’t matter what z holds as it’ll never be used. But notice what happens when z is empty.

if_x_then_y_else_z([True])(["y result"])(["z result"])
-- []

You get an empty list even though the z list isn’t needed since x holds a single True. But like I wrote up above, if any of the lists are empty, the result will be empty too.

You could try to get around this by lifting/upgrading a function that returns a list.

if_x_then_y_else_z(x)(y)(z) =
apply(pure(\ a -> if a then y else z))(x)
if_x_then_y_else_z([True])(["y result"])([])                           
-- [["y result"]]

⚠️ But now you’re stuck. You have nested effects or in this case, nested lists. Later on, you’ll see how the list monad deals with this situation.

💡The big thing to remember with applicative functor is that the lifted/upgraded function cannot change/influence the shape of the effect at any time. For list, the shape would be the size. For maybe the shape would be just or nothing. For either the shape would be left or right.

Without running the applicative functor, you can determine the resulting shape just by looking at the shape of the effects given. Given an empty list? You’re going to get an empty list. Given [1,2] and [3,4,5]? You’re going to get back a list with six items.

With monads it’s different. They can change the shape because they can look at the intermediate results and decide what to do next.

An applicative functor is a function.

Created with Blender and Gifcurry.
pure(function)(ignore) = function
pure(add1)(Nothing)(0)
-- 1
apply(wrapped_function)(function)(input) =
map(wrapped_function(input))(function)(input)
apply(pure(add1))(add1)(1)
-- 3
(pure(add) `apply` add1 `apply` add1)(1)
-- 4

The pure definition for functions is just the constant (const) function. The constant function takes two parameters and returns the first one ignoring the second one. So calling pure(add1)(Nothing) wraps and then unwraps add1. You can unwrap using any input you’d like as constant will just ignore it.

pure(add1)("Ignore this?")(1)
-- 2
pure(add1)(concat ["Ignore"," this?"])(1)
-- 2

Notice how I chained as many calls to apply as how ever many parameters the function took. I used one apply for add1 and two calls of apply for add.

add_x_y_z(x)(y)(z) = add(add(x)(y))(z)
add_x_y_z(2)(2)(2)
-- 6
(pure(add_x_y_z)
`apply` add1
`apply` add1
`apply` add1)(1)
-- 6

The three calls to apply — one for x, one for y, and one for z — returns 6 because

  • 1 gets added to 1 making x equal 2
  • 1 gets added to 1 making y equal 2
  • 1 gets added to 1 making z equal 2
  • and finally x, y, and z get added together for a total of 6.
apply(pure(add_x_y_z))(add1)(1)(2)(3)
-- 7
add_x_y_z(add1)(2)(3)
-- 7

You don’t have to chain a call to apply for every argument. In this example, I modified the x parameter with add1 but passed 2 as y and 3 as z directly to add_x_y_z.

add1(input) = 1 + input
sub2(input) = 2 - input
sub3(input) = 3 - input

(pure(add_x_y_z) `apply` add1 `apply` sub2 `apply` sub3)(4)
-- 2
add1(4) + sub2(4) + sub3(4)
-- 2
(1 + 4) + (2 - 4) + (3 - 4)
-- 2

If you do modify every parameter with apply, the resulting function will only accept one input which will go to every modified parameter. In this example, 2 went to every modified parameter of add_x_y_z.

first  = \ (a,b,c) -> a                                                               
second = \ (a,b,c) -> b
third = \ (a,b,c) -> c
new_add_x_y_z =
pure(add_x_y_z) `apply` first `apply` second `apply` third
new_add_x_y_z(1,2,3)
-- 6

If the form of your input data doesn’t match the parameter signature of your function, using apply to compose your function with some other functions comes in handy. In this example, the data was in a tuple so I composed add_x_y_z with first, second, and third to extract and then add together 1, 2, and 3.

extract_then_add_x_y_z(t) =
add_x_y_z(first(t))(second(t))(third(t))
extract_then_add_x_y_z(1,2,3)
-- 6

Granted, I could’ve done it like the example above but applicative functor gives us a nicely generalized interface for modifying the input to some function with other functions.

first  = \ (a,b) -> a
second = \ (a,b) -> b
new_add_x_y_z = pure(add_x_y_z) `apply` first `apply` second
new_add_x_y_z(1,2)(3)                                         
-- 6

If say, part of my data was in a tuple, I only have to reason about apply and pure instead of coming up with a specific function to handle this particular scenario.

An applicative functor is a functor that you lawfully define pure and apply for.

Created with Blender and Gifcurry.
pure
:: input -- Takes some input.
-> effect(input) -- Returns it inside an effect.
apply
:: effect(input -> output) -- Takes a function in an effect.
-> effect(input ) -- Takes input in an effect.
-> effect( output) -- Returns output in an effect.

In general, an applicative functor is a functor that you define apply and pure for. The apply function

  • takes a function inside an effect effect(input -> output),
  • takes input for that function inside an effect effect(input),
  • applies the function to the input,
  • and returns the function’s output inside the effect effect(output) .

💡 Compare it to map and you’ll see the subtle change — the function is now in the effect too.

map
:: (input -> output) -- Takes a function.
-> effect(input ) -- Takes input inside an effect.
-> effect( output) -- Returns output inside an effect.
apply
:: effect(input -> output) -- Takes a function in an effect.
-> effect(input ) -- Takes input in an effect.
-> effect( output) -- Returns output in an effect.

👀 Look again but in this condensed form.

map   ::  (i -> o) -> e(i) -> e(o)
apply :: e(i -> o) -> e(i) -> e(o)
-- ^ The big difference.

That’s the main and only difference between apply and map .

Lawfully, meaning applicative functor has laws too?

Yes. There are four applicative functor laws.

So applicative functor is an interface too?

Yes. If there are lawful definitions of pure and apply for some functor, it is an instance of applicative functor. In other words, applicative functor extends the functor interface with pure and apply.

I still don’t get applicative functor.

Applicative functor picks up where functor leaves off. Functor lifts/upgrades a function making it capable of operating on a single effect. Applicative functor allows the sequencing of multiple independent effects. Functor deals with one effect while applicative functor can deal with multiple independent effects. In other words, applicative functor generalizes functor.

In terms of an interface, applicative functor adds pure and apply to functor’s map.

What do you mean by independent effects?

Independent meaning some effect does not rely on the outcome of some other effect.

x = [1,2]
y = [3,4]
z = [5,6]
pure(add) `apply` (pure(add) `apply` x `apply` y) `apply` z  
-- [9,10,10,11,10,11,11,12]

In this example, we sequence three effects or lists. Neither x, y, nor z rely on the outcome of any other. Since they’re all independent, we could process the final result in parallel or simultaneously. This is one of the advantages to applicative functor.

[ 1+3+5=09,
1+3+6=10,
1+4+5=10,
1+4+6=11,
2+3+5=10,
2+3+6=11,
2+4+5=11,
2+4+6=12 ]

Now say we lift/upgrade the following function for the list instance.

add1_inside_effect(input) = pure(input + 1)

It will return a dependent effect/list dependent on some item input from some other list.

pure(add1_inside_an_effect) `apply` x                    
-- [[2],[3]]
pure(add) `apply` (pure(add1_inside_an_effect) `apply` x)
-- error!

Applicative functor only deals with independent effects. If you have a dependent effect, you’ll need monads as you’ll see next.

What is a monad?

A monad is a list.

Created with Blender and Gifcurry.
join(nested_lists) =
[element | list <- nested_lists, element <- list]
join([[1], [2, 3], [4, 5, 6]])
-- [1, 2, 3, 4, 5, 6]
join(apply(pure(\ x -> [x + 1, 1]), [1,2,3]))
-- [2, 1, 3, 1, 4, 1]
join(
apply(
pure(\ x -> [2, x * 3, x * 4]))(
join(
apply(
pure(\ x -> [x + 1, 1]))(
[1,2,3]))))
-- [2,6,8,2,3,4,2,9,12,2,3,4,2,12,16,2,3,4]

The big and only addition for the monad interface is join. join flattens or better put, peels off an effect from two nested effects. For the list instance, join is just concat.

Join only deals with nested effects that are the same type and nested one level deep. So for example, [[1], [3,4]], just(just(1)), right(right(2)) but not [just(1), just(left(1))], [1, [2]], [[[1]]], etc.

if_x_then_y_else_z =
pure(\ a -> if a then y else z) `apply` x
if_x_then_y_else_z([True])(["y result"])([])                           
-- [["y result"]]

When we previously lifted/upgraded a function that returned a list (to get around z being empty), we ended up with a nested list. Back then we were stuck but join allows us to keep on truckin’ 🚚. Now you can pick either the y or z list based on the items in x and not end up with a nested list.

if_x_then_y_else_z(x)(y)(z) =
join(pure(\ a -> if a then y else z) `apply` x)
if_x_then_y_else_z(
[True,False,False,True])(
["y result"])(
["z result"])
-- [ "y result", True so return y.
-- "z result", False so return z.
-- "z result", False so return z.
-- "y result" ] True so return y.

💡 Therein lies the power of monads, they allows us to pick the next effect based on the result from a previous effect. In other words, monad allows us to sequence dependent effects or better put, sequence functions that return effects.

⚠️ Note that “pick the next effect” doesn’t mean you can, at some point, change the type of effect you return. For example, you can’t suddenly decide to return a maybe if you’re operating in the list monad. A monad is also a functor, so in the end, the effect must remain intact with only its results potentially altered.

A monad is a function.

Created with Blender and Gifcurry.
join(wrapped_function)(input) =
wrapped_function(input)(input)
join(apply(pure(add))(add1))(1) 
-- 3
add(add(1)(1))(1)
-- 3
add(2)(1)
-- 3
(1 + 1) + 1
-- 3
divide(numerator)(denominator) = numerator / denominator
join(
apply(
pure(divide))(
join(
apply(
pure(add))(
add1))))(2)
-- 2.5
divide(
add(
add(
1)(
2))(
2))(
2)
-- 2.5
divide(
add(
3)(
2))(
2)
-- 2.5
divide(
5)(
2)
-- 2.5
(((1 + 2) + 2) / 2)
-- 2.5

The function monad allows you to sequence or compose functions together. The first function in the chain can only take one parameter and the rest have to take two parameters.

After you’re done composing functions together using join, apply, and pure, the returned function will only take one input. This single input will start a chain reaction.

  • The first function takes only one parameter.
  • It receives the input and returns some output.
  • The second function takes two parameters.
  • It receives the output from function one, the input, and returns some output.
  • The third function takes two parameters.
  • It receives the output from function two, the input, and returns some output.
  • The fourth function…and on and on for how ever many functions you sequenced.

A monad is an applicative functor that you lawfully define join for.

Created with Blender and Gifcurry.
join
:: effect(effect(data))
-> effect(data)

In general, a monad is just an applicative functor you define join for.

So monads are just applicative functors with join added to the interface?

Yes.

How many laws are there for monad?

There are three monad laws.

What about return and bind?

Bind is just a convenience function that composes join, apply, and pure.

bind(effect)(function) = join(apply(pure(function))(effect))
[1,2,3] `bind` \ x -> [x + 2]
-- [3,4,5]
[1,2,3] `bind` \ x -> [x + 2] `bind` \ y -> [y * 3]       
-- [9,12,15]
[1,2,3] `bind` \ x -> pure(x + 2) `bind` \ y -> [y * 3, 0] 
-- [9,0,12,0,15,0]
(add1 `bind` add `bind` divide)(2)
-- 2.5

Or if you prefer, bind composes join and map together.

bind(effect)(function) = join(map(function)(effect))
[1,2,3] `bind` \ x -> pure(x + 2) `bind` \ y -> [y * 3, 0] 
-- [9,0,12,0,15,0]

return is just pure.

return = pure

Can you summarize them for me?

Created with Blender and Gifcurry.
  • Functor lifts or upgrades a function, allowing it to operate on a single effect, leaving the effect intact after it’s done. It requires a lawful map definition.
  • Applicative functor builds on or generalizes functor, allowing you to sequence multiple independent effects. It requires a lawful pure and apply definition.
  • Monad builds on or generalizes applicative functor, allowing you to sequence independent and/or dependent effects. It requires a lawful join definition.
map   ::  (i ->   o ) -> e(i) -> e(o) -- Functor
apply :: e(i -> o ) -> e(i) -> e(o) -- Applicative
bind :: (i -> e(o)) -> e(i) -> e(o) -- Monad
^ ^

If you’d like to see what you can do with functors, applicatives, and monads, check out Movie Monad and Gifcurry — two desktop GUI apps created with Haskell, a purely functional programming language.

For more information about functors, applicatives, monads, and other functional programming patterns, take a look at Typeclassopedia by Brent Yorgey.