Functional Programming in JavaScript, Part 3: Introduction to Functors and Monads
This is part 3 in a series on Functional Programming in javascript
After discovering the Unit and the Monoid, it's time to talk about Functors
and Monads
. I know, those words can be frightening. Their definitions are frightening, too:
Functor
A morphism from a source category to a target category which maps objects to objects and arrows to arrows, in such a way as to preserve domains and codomains (of the arrows) as well as composition and identities.
Monad
How is that supposed to be helpful? Let's forget those awful definitions and start with an example of problem the functor and monad resolve.
Checking Input Properties
Let's say I want to increment a number.
const increment = v => v + 1;
const a = 5;
increment(a); // 6
Simple, right? But then someone calls the increment
function with an incorrect argument:
increment("5"); // '51'
// or like this
increment({ v: 5 }); // "[object Object]1"
So to handle this case, I need to add a type check to the function:
const increment = v => {
if (typeof v !== "number") {
return NaN; // NaN fit our purpose here, we received not a number, so we return just that
}
return v + 1;
};
increment("5"); // NaN
increment({ v: 5 }); // NaN
Good. Now, let's say that I need another function to double a number. Again, to be safe I need to repeat the same type check:
const double = v => {
if (typeof v !== "number") {
return NaN;
}
return v * 2;
};
In fact each time I manipulate a value, I should do some verifications:
- Is it the right type?
- Is it here?
- Was there an error?
And sometimes, the value is not even readily available, it comes in a callback, or not, maybe we will get an error.
Echanging The Function And Value Roles
What if instead of passing the value to the functions, we passed the functions to the value? Or rather to a placeholder for the value.
I mean, a value already knows some important things about the functions that will be called with it:
- If I am null, there is no need to execute the function.
- If I am not of the correct type, I should skip all functions passed to me.
- If I am an error, I should not execute this function, but I should execute this other one instead.
- If I am asynchronous, I should call this function once I arrive
In short, the value knows best what to do with the function.
This is the basic idea behind a pattern called Functor: a placeholder for a value, so that you pass the function to the value, and not the other way around. We are in functional programming after all: we want to manipulate functions, not values.
Let's write a placeholder for a number value: a NumberBox.
const NumberBox = number => ({
applyFunction: fn => NumberBox(fn(number)),
value: number,
});
NumberBox(5)
.applyFunction(v => v * 2)
.applyFunction(v => v + 1).value; // 11
For now, this code still suffers from the same problem on non-number arguments. So let's enhance the placeholder to check that it holds a numeric value before applying the function.
const NumberBox = number => ({
applyFunction: fn => {
if (typeof number !== "number") {
return NumberBox(NaN);
}
return NumberBox(fn(number));
},
value: number,
});
NumberBox(5)
.applyFunction(v => v * 2)
.applyFunction(v => v + 1).value; // 11
NumberBox({ v: 5 })
.applyFunction(v => v * 2)
.applyFunction(v => v + 1).value; // NaN
This applyFunction
method looks a lot like another function that we often use, doesn't it?
[1, 2, 3].map(v => v + 1);
That's right, Array.map()
works the same: it takes values from an array instead of a box, but it applies the function all the same.
From now on let's rename applyFunction
to map
, as we map a function over a value.
const NumberBox = number => ({
map: fn => {
if (typeof number !== "number") {
return NumberBox(NaN);
}
return NumberBox(fn(number));
},
value: number,
});
And, just like that, I have created a functor, which is a value placeholder offering a map
function to execute functions on it.
So to recapitulate, I have a box that holds a value and can apply a function to it thanks to a map
method. This allows adding custom logic to the function application. This way I manipulate a function, not a value. And with this pattern, I can do a lot more.
Using The Functor Pattern For Type Checking
Let's generalize the NumberBox
function to handle any type checking, not just numbers. I'll create a TypeBox
function, that generates a functor based on a test (or predicate, to use a more exact word):
const TypeBox = (predicate, defaultValue) => {
const TypePredicate = value => ({
map: fn =>
predicate(value) ? TypePredicate(fn(value)) : TypePredicate(defaultValue),
value,
});
return TypePredicate;
};
So now I can recreate the NumberBox
by passing a number check as predicate, and NaN
as default value:
const NumberBox = TypeBox(value => typeof value === "number", NaN);
NumberBox(5)
.map(v => v * 2)
.map(v => v + 1).value; // 11
NumberBox({ v: 5 })
.map(v => v * 2)
.map(v => v + 1).value; // NaN
I can also create a StringBox
, a functor for strings:
const StringBox = TypeBox(value => typeof value === "string", null);
StringBox("world")
.map(v => "Hello " + v)
.map(v => "**" + v + "**").value; // '**Hello, world**'
StringBox({ v: 5 })
.map(v => "Hello " + v)
.map(v => "**" + v + "**").value; // null
And you can imagine how this applies to all kinds of type checks.
The map
method has an interesting property: It allows to compose functions. Said otherwise:
const double = v => v * 2;
const increment = v => v + 1;
NumberBox(5)
.map(double)
.map(increment).value; // 11
// will have the same result as :
NumberBox(5).map(v => increment(double(v))).value; // 11
In fact, any map
implementation must make sure to have this property. In a functional paradigm, we want to compose small functions into more complex ones.
There is another thing that I must make sure of: map
must not have any side effects. It must only change the functor value, and nothing else. How can I make sure of this? By testing that mapping the identity function (v => v
) returns the exact same functor
. This is called the identity law.
NumberBox(5).map(v => v) == NumberBox(5)
I will not check these laws for the other examples, but feel free to check them for yourself.
Maybe Another Example
Sometimes, making a function robust isn't about checking the input type, it's about checking that the input exists. Like when a function accesses a property:
const street = user.address.street;
To handle the case where user
or user.address
are not set, I need to add existence checks:
const street = user && user.address && user.address.street;
I can use the functor pattern to handle such cases. The idea is to create a placeholder to hold sometimes a value, and sometimes nothing. Let's call this functor Maybe
.
const isNothing = value => value === null || typeof value === "undefined";
const Maybe = value => ({
map: fn => (isNothing(value) ? Maybe(null) : Maybe(fn(value))),
value,
});
Now it's safe to call properties on a non-object:
const user = {
name: "Holmes",
address: { street: "Baker Street", number: "221B" },
};
Maybe(user)
.map(u => u.address)
.map(a => a.street).value; // 'Baker Street'
const homelessUser = { name: "The Tramp" };
Maybe(homelessUser)
.map(u => u.address)
.map(a => a.street).value; // null
Tip: Calling the value
property at the end isn't very natural in functional programming. I can augment the Maybe
functor with a getter that accepts a default value as argument:
const isNothing = value => value === null || typeof value === 'undefined';
const Maybe = value => ({
map: fn => isNothing(value) ? Maybe(null) : Maybe(fn(value)),
getOrElse = defaultValue => isNothing(value) ? defaultValue : value,
});
As for accessing object properties, it can get even better using a get
helper:
const get = key => value => value[key];
const getStreet = user =>
Maybe(user)
.map(get("address"))
.map(get("street"))
.getOrElse("unknown address");
getStreet({
name: "Holmes",
firstname: "Sherlock",
address: {
street: "Baker Street",
number: "221B",
},
}); // 'Baker Street'
getStreet({
name: "Moriarty",
}); // 'unknown address'
getStreet(); // 'unknown address'
Either Right or Wrong
What about error handling? If the function mapped on a functor throws an error, the functor should be able to handle it.
For instance, let's consider the code required to validate a user email. To be precise, I want to validate an email address only if it is given, otherwise, I ignore it and return null
.
const validateEmail = value => {
if (!value.match(/\S+@\S+\.\S+/)) {
throw new Error("The given email is invalid");
}
return value;
};
validateEmail("foo@example.com"); // 'foo@example.com'
validateEmail("foo@example"); // throw Error('The given email is invalid')
I'm not fond of that throw
, because it interrupts the execution flow. What I'd like is a validateEmail
function that doesn't throw an error, but may return either a value or an error. How to do this with functors?
I need to return a different functor based on a try/catch
: either a functor that allows me to continue to map, or a functor that would not change its value anymore (if it's an error). I'll call these two functors Left
and Right
. The validateEmail
function would look like the following:
const validateEmail = value => {
if (!value.match(/\S+@\S+\.\S+/)) {
return Left(new Error("The given email is invalid"));
}
return Right(value);
};
The Left
functor should never change its value anymore, whatever function is passed to its map
:
const Left = value => ({
map: fn => Left(value),
value,
});
Left(5).map(v => v * 2).value; // 5
As for the Right
functor, it simply applies a function to its value, transparently. You could call it the Identify
functor, too.
const Right = value => ({
map: fn => Right(fn(value)),
value,
});
Right(5).map(v => v * 2).value; // 10
And now I can chain map
calls on my email value:
validateEmail("foo@example.com") // valid email, return Right functor
.map(v => "Email: " + v).value; // 'Email: foo@example.com'
validateEmail("foo@example") // invalid email, return Left functor
.map(v => "Email: " + v).value; // Error('The given email is invalid')
The Left
and Right
functors form what we call an Either
.
Catching Errors
The validateEmail
returns either a result or an error, but doesn't allow me to do something specific in case of an error. For instance, I may want to catch the Error
and log its message
property.
I'll add a catch()
function to both the Left
and Right
functors to allow me to do just that:
// on a Right, catch() should do nothing
const Right = value => ({
map: fn => Right(fn(value)),
catch: () => Right(value),
value,
});
// catch is ignored on a right
Right(5).catch(error => error.message).value; // 5
// on a Left, catch() should apply the function, and return a Right to allow further mapping
const Left = value => ({
map: fn => Left(value),
catch: fn => Right(fn(value)),
value,
});
Left(new Error("boom")).catch(error => error.message).value; // 'boom'
For better reusability, I combine these Left
and Right
functors into a generic tryCatch
function:
const tryCatch = fn => value => {
try {
return Right(fn(value)); // everything went fine we go right
} catch (error) {
return Left(error); // oops there was an error let's go left.
}
};
And now, I can use this new tryCatch
function to decorate the initial implementation of validateEmail
, and get either a Left
or a Right
functor in return.
const validateEmail = tryCatch(value => {
if (!value.match(/\S+@\S+\.\S+/)) {
throw new Error("The given email is invalid");
}
return value;
});
validateMail("foo@example.com")
.map(v => "Email: " + v)
.catch(get("message")).value; // 'Email: foo@example.com'
validateMail("foo@example")
.map(v => "Email: " + v)
.catch(get("message")).value; // 'The given email is invalid'
Combining Functors
Now, let's try to validate not an email string, but a user object that maybe has an email. I do not need to validate the email if the user has no email property.
Let's use Maybe
and combine it with Either
:
const validateUser = user =>
Maybe(user)
.map(get("email"))
.map(v => validateMail(v).catch(get("message")));
validateUser({
firstName: "John",
email: "foo@example.com",
}); // Maybe(Right('foo@example.com'))
validateUser({
firstName: "John",
email: "foo@example",
}); // Maybe(Left('The given email is invalid'))
validateUser({
firstName: "John",
}); // Maybe(null)
I always get a Maybe
, but its value is either a Left
or a Right
functor, or even a null value. So in order to get the value, I have to test Maybe
value to know what to do with it.
const validateUserValue = user => {
const result = validateUser(user).value;
if (value === null || typeof value === "undefined") {
return null;
}
return result.value;
};
This is bad: all the code I worked so hard to remove is coming back for revenge. You can imagine that it gets worse if I try to combine more functors, ending up with .value.value.value.value
...
How can I handle this crisis?
Chain To The Rescue
I need a method allowing me to extract a value of a functor, even though this value is wrapped inside another functor.
I need a flatten
method that removes the inner functor, keeping only its value.
const Maybe = value => ({
// we could return the value, but then we would sometimes switch functor type.
// This way Maybe.flatten will always return a Maybe
flatten: () => isNothing(value) ? Maybe(null) : Maybe(value.value),
...,
});
const validateUser = user => Maybe(user)
.map(get('email'))
.map(v => validateMail(v)
.catch(get('message'))
)
.flatten()
// since now I will always have a simple Maybe, I can use getOrElse to get the value
.getOrElse('The user has no mail');
validateUser({
firstName: 'John',
email: 'foo@example.com',
}); // 'foo@example.com'
validateUser({
firstName: 'John',
email: 'foo@example',
}); // 'The given email is invalid'
validateUser({
firstName: 'John',
}); // 'The user has no mail'
So now I can call .getOrElse
and get the value directly. But that's still not ideal. You see, in functional programming, you often need to map and then flatten, or to flatMap for short. But the term "flatMap" describes what the procedure does, not what it is used for. This operation is used to chain functors, so let's call it that: chain
.
const Maybe = value => ({
flatten: () => isNothing(value) ? Maybe(null) : Maybe(value.value),
// using the named function form instead of the fat arrow function
// because I need access to this
chain(fn) {
return this.map(fn).flatten();
},
...,
});
const validateUser = user => Maybe(user)
.map(get('email'))
.chain(v => validateMail(v)
.catch(get('message'))
)
.getOrElse('The user has no mail');
validateUser({
firstName: 'John',
email: 'foo@example.com',
}); // 'foo@example.com'
validateUser({
firstName: 'John',
email: 'foo@example',
}); // 'The given email is invalid'
validateUser({
firstName: 'John',
}).value; // 'The user has no mail'
Note that the two names are used in functional programming, so flatMap
== chain
.
Here Comes The Monad
By the way, a functor that can flatten itself with a chain
method is called a Monad. So Maybe
is a monad. Not so scary, right?
Like map
, chain
must respect a few laws to ensure that monads combine correctly.
- Left identity
Monad(x).chain(f) === f(x);
// f being a function returning a monad
Chaining a function to a monad
is the same as passing the value to the function.
This ensures that the encompassing monad
is totally removed by chain
.
- Right identity
Monad(x).chain(Monad) === Monad(x);
Chaining the monad constructor should return the same monad. This ensures that chain
has no side effect.
- Associativity
monad.chain(f).chain(g) == monad.chain(x => f(x).chain(g));
// f and g being functions returning a monad
`Chain` must be associative: Using `.chain(f).chain(g)`, is the same as using `.chain(v => f(v).chain(g))`.
This ensures that we can `chain` function that uses `chain` themselves.
Conclusion
You can play with the code of this tutorial in the CodeSandbox below. It uses a slightly modified example: try to understand why it's better than what was explained earlier.
codepen.io might track you and we would rather have your consent before loading this.
I hope I have demystified functors and monads, and shown how they can be useful. Next time, I will see how we can use monad to handle side effect in a functional way.