Functors and monads are concepts that are often misunderstood and over-explained at the same time. Many tutorials and blog posts focus on why these concepts are useful instead of what they actually are. This is understandable since it is actually not required to know what these concepts are in order to take advantage of them.
In this post, we first look into what a functor and a monad are. After that, we switch over to what we can achieve by using them.
Functor
The word functor is the name of a specific interface which describes the following property: when we have something of type a
, and we have a way to take a
and map it to b
, we can have something of type b
[1]. Something needs to be a source of a value of some type. The word "source" is chosen deliberately to represent an abstract thing because it can be pretty much anything. Let's take a look at a couple of examples of a source of type string
:
// the values are accessed by iteration
export type ArraySource = Array<string>;
// the value is accessed through property `a`
export interface RecordSource {
a: string;
}
// the value is accessed through a function call
export interface FunctionSource {
(): string;
}
All of the above are sources of type string
because there is a way to access the value. Array, Record, and Function are functors because they specify a way to map their enclosed values to a potentially different type. We can also think of functors as type classes specifying some kind of a map function. That function does not necessarily have to be called map
but needs to behave like one. For example, the type Array
is a functor because it defines a map
function.
// source of values `a` (Array<number>)
const numbers = [1, 2, 3];
// mapping of `a` (number) to `b` (string)
const toString = (a: number): string => String(a);
// source of values `b` (Array<string>)
export const result = numbers.map(toString);
Array#map
has the ability to take an a
(of any type) from an instance of Array by iterating over its values, apply a mapping function on each a
, and produce a new instance of Array holding values of b
of type that is dictated by the return type of our mapping function.
How would a Function#map
look like?
interface Fn<A> {
(): A;
}
interface FnMap {
<A, B>(fn: (a: A) => B): (a: Fn<A>) => Fn<B>;
}
const functionMap: FnMap = (fn) => (a) => {
return () => fn(a());
};
// source of values `a` (Fn<number>)
const makeNumber = () => 42;
// mapping of `a` (number) to `b` (string)
const toString = (a: number): string => String(a);
// source of values `b` (Function<string>)
export const result = functionMap(toString)(makeNumber);
functionMap
has the ability to take an a
(of any type) from an instance of type Function
by calling it, apply a mapping function on its return value, and produce a new instance of Function
holding a value of b
of type that is dictated by the return type of our mapping function.
In both examples, we use the exact same mapping function (toString
) without having to change anything about it. The same function can then be applied to both functors: Array
and Function
. In fact, we can supply that mapping function to any functor's map function exactly because the term functor describes a property, and not a noun. Reusing code without additional modifications is extremely powerful.
Monad
Just like functor, the word monad is also the name of a specific interface. A monad is something that fulfils the following:
- it is a functor, i.e. implements a
map
function - it can be chained, i.e. implements a
flatMap
function - it can be constructed, i.e. implements an
of
function - it abides by the Monad laws
Chainability
The definition of chainability is very similar to functor's mappability. The difference is highlighted in bold letters: when we have something of type a
, and a way to map a
onto the same type of something of type b
, we can have something of type b
[1]. Same as with functor, something needs to be a source of a value of some type. We can also think of monads as type classes specifying some kind of a flatMap function. That function does not necessarily have to be called flatMap
but needs to behave like one. For example, the type Array
, besides being a functor, is also a monad because it defines a flatMap
function.
// source of values `a` (Array<string>)
const expressions = ["Functors and monads", "are easy"];
// mapping of `a` (string) to the same type of source (Array) of `b` (Array<string>)
const splitByWords = (a: string): string[] => a.split(" ");
// source of values `b` (Array<string>)
export const words = expressions.flatMap(splitByWords);
// ["Functors", "and", "monads", "are", "easy"]
So, how does our example with flatMap
fit the definition? Let's break it down:
- "when we have something of type
a
":expressions
- the type of the source isArray<string>
. - "and a way to map
a
onto the same type of something of typeb
":splitByWords
mapsstring
ontoArray<string>
(matches the type from above). - "we can have something of type
b
":expressions.flatMap(splitByWords)
producesArray<string>
becauseflatMap
knows two things:- how to provide
a
to the mapping function, - how to flatten the intermediate results produced by the mapping function.
- how to provide
Evaluating expressions.map(splitByWords)
would produce a value of type Array<Array<string>>
. Hence, things that implement a function like map
but don't implement a function like flatMap
cannot be called monads.
Constructability
A monad also needs to implement a function that may take a value and returns an instance of the monad encapsulating that value. Many times these functions will be called of
, as in Task.of(42)
, but can also be called anything else, e.g.: left
, right
, some
, none
, etc.
Monad laws
The explanation of monad laws is beyond the scope of this series. However, if you're interested, you can read Monad laws for regular developers written by Miklós Martin. It provides a nice explanation using both Scala and JavaScript examples.
Why are functors and monads useful?
In my opinion, they are useful because of their following three innate characteristics:
- They are polymorphic.
- They abstract away some boilerplate code such as iteration and control flow logic.
- They are composable.
Polymorphism
Polymorphism just means that an object can take up multiple forms. For example, the type Array
can be of string
s, number
s or any other type.
Functions like map
and flatMap
don't care what the type of the value is. That is the responsibility of the mapping function. With map
and flatMap
, we can easily go from an Array<number>
to an Array<string>
.
const numbers = [1, 2, 3];
export const strings = numbers.map(String);
// ["1", "2", "3"]
This is a great property to have because we don't have to implement map
and flatMap
functions for every possible type of Array out there.
Useful abstractions
Functors and monads abstract away some common boilerplate code such as iteration and control flow logic. By using functors and monads, we repeat less code. Also, we tend to write more declarative code instead of imperative.
Imperative code is more prescriptive, i.e. we say how a certain thing should be implemented.
Declarative code is more descriptive, i.e. we say what a certain thing should do. People who write code are humans, and humans make mistakes. So, the less we have to describe how things should be implemented, the more of the following may apply to our code:
- has fewer bugs,
- runs faster,
- is more secure.
Obviously, this applies only if the functor/monad implementation is well tested, optimized for speed and implemented with security in mind.
Iteration and Control flow
The previously shown code examples demonstrate how map
and flatMap
both hide the iteration logic. To demonstrate the control flow logic being abstracted away, we'll need to assume something about how Array's map
function is implemented.
export const map =
<A, B>(fn: (a: A) => B) =>
(x: A[]): B[] => {
if (x.length === 0) return [];
// pretend to be the rest of map's implementation
return x.map(fn);
};
This is a simple and not very useful optimization but it serves its purpose well.
[]
.map((x) => x + 1)
.map((x) => x * 2)
.map((x) => x % 2);
// []
Every time map is called, an empty array is returned early skipping possibly expensive and unnecessary computations. The real value of abstracting the control flow logic will be more apparent when we'll look into the Either
and Option
monads in the upcoming chapters.
Function composition
Because of how functor and monad is defined, it lends itself to function composition very well. Remember how Array#map
and Array#flatMap
work? They both return an instance of Array
. This means, we can keep calling map
and flatMap
in succession:
const splitByWords = (a: string): string[] => a.split(" ");
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;
const expressions = ["Functors and monads", "are easy"];
export const result = expressions.flatMap(splitByWords).map(len).map(double);
// [ 16, 6, 12, 6, 8 ]
Function composition in this example is achieved by method chaining. If we want, we can create our own map
with a slightly different interface and use pipe
from the previous chapter:
import { pipe } from "fp-ts/lib/function.js";
const map =
<A, B>(f: (_: A) => B) =>
(a: A[]): B[] =>
a.map(f);
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;
export const result = pipe(
["Functors", "and", "monads", "are", "easy"],
map(len),
map(double)
);
// [ 16, 6, 12, 6, 8 ]
The advantage of redefining map
's interface is that we can "chain" functions other than map
and flatMap
as well. Luckily for us, fp-ts
already did that for both map
and flatMap
, so we could have written our example just as:
import { array } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
const splitByWords = (a: string): string[] => a.split(" ");
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;
export const result = pipe(
["Functors and monads", "are easy"],
array.chain(splitByWords), // flatMap is called chain in fp-ts
array.map(len),
array.map(double)
);
// [ 16, 6, 12, 6, 8 ]
Wrap-up
- Functors and monads can be thought of as interfaces.
- Functor is an interface specifying a function similar to
Array#map
. - Monad is a functor that also:
- specifies a function similar to
Array#flatMap
, - has a constructor:
Array()
, - abides by the Monad laws.
- specifies a function similar to
- Functors and monads are useful because they are:
- polymorphic,
- reduce code repetition,
- compose well.
The next part of this series delves into a very specific type of monad implemented in fp-ts
: the Either monad.
Top comments (1)
It has just been brought to my attention that for something to be a monad, it also has to implement
of
(i.e. sort of a constructor) besides themap
andflatMap
functions. Furthermore, all three have to satisfy the monad laws.Because of the above, Promise is not a monad. I will try to find time to correct these mistakes and provide better examples as soon as possible. Million thanks to Michael Arnaldi for bringing this to my attention :-)