Functional Programming in JS: Functor - Monad's little brother
mpodlasin
Posted on October 17, 2020
So you have heard about monads. You've read some tutorials, seen some examples, yet you still can't quite wrap your head around them.
It's interesting, because for me the concept of monad never seemed that challenging. And I believe that's because before learning about monads, I've learned about functors first.
The thing is, learning about monads without knowing and understanding functors, is like learning about Redux without knowing and understanding React. It's just doesn't make sense.
Functors are much simpler than monads. On top of that, all instances of monads are actually functors as well. Moreover, functors are actually interesting on their own. After learning them, you will start seeing functors everywhere, which will enable you to create elegant programming patterns and APIs.
So if you ever had trouble getting into programming with monads, read my article about functors. We will start with a bit of light theory and later we will show a practical example of how functors can be utilized to write cleaner, more readable code.
If, after reading this article, you decide that it was helpful to you, subscribe to me on Twitter for more content on JavaScript, React and functional programming.
Let's get started!
What are functors?
In functional programming we have all those weird, esoteric concepts with fancy names. Functors. Monads. Applicative functors. Semigroups. Monoids.
They sound abstract and mathematical (not without a reason), which scares off newcomers.
So what really are those things?
You can think of them as nothing more than an adequate of design patterns or interfaces in object oriented programming. They are simply a way to notice some commonality in the code we are writing and making this commonality explicit.
So, for example, a very popular pattern and an interface in object oriented languages is an iterable. It's simply a thing that can be iterated over. Even simpler - a thing that can be used in a for loop.
When programmers started writing programs with for loops, they have noticed that you can iterate over many different data structures. Arrays, linked lists, various types of dictionaries, graphs, generators etc. The list goes on.
Those data structures are often vastly different in nature and serve completely different purposes, but they have a thing in common - we can write a for loop which somehow iterates over their elements.
So those weird things in functional programming, like functors and monads, were created with a similar viewpoint. We notice that there are some commonalities in our code, so we actually introduce them to the codebase in an explicit way.
This makes programming easier. It's simpler to manipulate various data structures if they have similar APIs. Imagine each car having a completely different steering mechanism. It would be so tough to switch between cars! But because cars, no matter how different - from tiny minicars to massive trucks - are steered very similarly, it makes driving them much easier.
In the same way, using data structures which obey common interfaces is much easier as well.
On top of that, if we have defined a general interface, we can now try to write code that works on all instances of that interface. For example it should be possible to transform each instance of an iterable into a list of specified length. After all, we can simply iterate over a data structure with a for loop and step by step place it's elements inside a new list.
We can write a function like that just once, instead of writing it each time for each instance of the iterable interface. Functors and monads have these capabilities as well. For example Haskell's standard library is full of functions that work on all instances of various abstract interfaces. This makes reusing code very easy, eliminating the need to write similarly looking functions many times.
Concept of a functor on JS examples.
So with that introduction out of the way, we are now ready to present what exactly are functors.
Functors are simply things that can be mapped over.
This might seem like a very abstract sentence, so let's motivate it with a few examples.
When you hear about "mapping things", you probably immediately think about map
method available on JavaScript arrays. This method allows you to take a function and apply it on each element of the array. A new array gets created and its elements are results returned from successive calls to our function.
Let's say we want to transform an array of numbers into an array of strings. map
method allows us to do it easily:
const arrayOfNumbers = [1, 2, 3];
const arrayOfStrings = arrayOfNumbers.map(num => num + '');
The arrow function num => num + ''
converts a number to a string in a straightforward way.
So when we apply this function via map to our array, we get as a result ["1", "2", "3"]
. Easy.
It's also interesting to note that if the array is empty, map
still works properly. Since there are no elements to map, it just returns an empty array again.
This might not sound like much, but note that a corner case - an empty array - is handled for us here, without having to manually check if there are actually any elements in the array.
So - according to our definition - because we can map arrays, array is indeed an instance of a functor.
Are there any other functors in native JavaScript?
You might be surprised to find out that Promises are also functors. "But why? - you might ask - Promises don't have a map method on them like arrays do!"
And that's true. But note that then
method on Promises also allows you to map a value stored inside a Promise. Let's say that now instead of the array of numbers, we have a Promise that stores a number. We can use the same function that we used on the array to change that number into a string:
const promiseWithNumber = Promise.resolve(5);
const promiseWithString = promiseWithNumber.then(num => num + '');
As a result we get a Promise that resolves to a value "5"
.
Compare the code with Promises and with arrays and note just how similar it is in both syntax and behavior:
const arrayOfStrings = arrayOfNumbers.map(num => num + '');
const promiseWithString = primiseWithNumber.then(num => num + '');
What obfuscates this similarity is the fact that Promise then
method is a do-it-all method. It is used for mapping, for side effects and for monad-like behavior.
From a functional point of view it would be a cleaner design if Promises simply had a dedicated map
method that obeyed some stricter rules:
- you couldn't (or at least shouldn't) do any side effects inside it,
- you couldn't (or at least shouldn't) return a Promise again inside that function.
Then the similarity would be much more obvious:
const arrayOfStrings = arrayOfNumbers.map(num => num + '');
// now it's a map!
const promiseWithString = promiseWithNumber.map(num => num + '');
But this doesn't change the fact that with then
you still can achieve a functor-like behavior. So for all intents and purposes it's totally okay to think about a Promise as an another instance of a functor interface.
Coming up with our own functors.
Honestly I don't know any other good examples of functors in native JavaScript. If you do, please let me know in the comments!
But this doesn't mean we are done. We can introduce functors in our own, custom code. In fact, this will be the biggest practical advantage of knowing functors for you. Introducing functor behavior to your data structures will allow you to write cleaner and more reusable code, just how map
allows you to do it with arrays.
The first approach could be to introduce mapping to some other native JavaScript data structure.
For example there is no native map
method for JavaScript objects. That's because when writing such method you would have to make some not so obvious design decisions. But because we are writing our own map
here, we can just do whatever we wish.
So how mapping of an object could look like? It's probably the best to think of an example. Let's assume we still want to use our num => num + ''
function, which maps numbers to strings.
If we get an object where the values are numbers:
const objectWithNumbers = {
a: 1,
b: 2,
c: 3
};
we want to return an object of the same shape, but with strings instead of numbers:
const objectWithStrings = {
a: "1",
b: "2",
c: "3",
};
What we can do, is to use an Object.entries
method to get both keys and values of numbersObject
. Then, based on those values, we will create a new object, with values mapped by num => num + ''
function.
Because it is a bad practice to add new methods to native JS prototypes, we will simply create a mapObject
function, which will accept two arguments - an object which we want to map and a function that does the actual mapping:
const mapObject = (object, fn) => {
const entries = Object.entries(object);
const mappedObject = {};
entries.forEach(([key, value]) => {
// here is where the mapping is happening!
mappedObject[key] = fn(value);
});
return mappedObject;
};
Then, if we run this example:
const objectWithNumbers = {
a: 1,
b: 2,
c: 3
};
const objectWithStrings = mapObject(objectWithNumbers, num => num + '');
we will indeed get a result that we expect.
So our collection of functors just got bigger. We can map arrays, promises and objects:
const arrayOfStrings = arrayOfNumbers.map(num => num + '');
const promiseWithString = promiseWithNumber.then(num => num + '');
const objectWithStrings = mapObject(objectWithNumbers, num => num + '');
In the spirit or reusability, let's give a name to our num => num + ''
function and use that name in the examples:
const numberToString = num => num + '';
const arrayOfStrings = arrayOfNumbers.map(numberToString);
const promiseWithString = promiseWithNumber.then(numberToString);
const objectWithStrings = mapObject(objectWithNumbers, numberToString);
This way you can see just how reusable and composable our code is now. We can use numberToString
function not only directly on numbers, but also on anything that is a functor containing numbers - arrays of numbers, promises with numbers, objects with numbers etc.
Let's create yet another instance of a functor.
This time, instead of creating a map function for already existing data structure, we will create our own data structure and ensure that it will be a functor, by providing it with a map
method.
We will write a Maybe data structure, which is extremely popular in functional programming. Perhaps you have heard it being called "Maybe monad". And indeed, Maybe is a monad, but it is also a functor, and that's the aspect of Maybe that we will focus on in this article.
Maybe is a data structure which represents a value that may or may not exist. It is basically a replacement for null
or undefined
. If something can be either null
or undefined
, we will use Maybe instead.
And indeed, in our implementation of Maybe we will simply use null
to represent a value that does not exist:
class Maybe {
constructor(value) {
this.value = value;
}
static just(value) {
if (value === null || value === undefined) {
throw new Error("Can't construct a value from null/undefined");
}
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
}
As you can see, Maybe is simply a wrapper for a value, with two static methods.
Maybe.just
allows you to create a Maybe data structure with an actual value inside (that's why we do checks for null
and undefined
).
On the other hand, Maybe.nothing
simply creates a Maybe with a null value inside (which we interpret as "no value").
At this point such data structure might not seem very useful. That's precisely because it is not a functor yet! So let's make it a functor, by writing a map method:
class Maybe {
// nothing changes here
map(fn) {
if (this.value === null) {
return this;
}
return new Maybe(fn(value));
}
}
Note that map method here is immutable - it does not modify an instance on which it is called, but rather it creates a new instance of Maybe or just returns the previous, unmodified value.
If Maybe has a null
inside, it simply returns the same value - a Maybe with null
.
If however Maybe contains some actual value, then map
calls fn
mapper on that value and creates a new Maybe with a mapped value inside.
This might seem like a lot, so let's play around with our newly created Maybe data structure:
const maybeNumber = Maybe.just(5);
const maybeString = maybeNumber.map(numberToString);
Here we create a Maybe with an actual value inside - a number 5. Than we can use numberToString
to map it to a Maybe with a string "5"
inside.
But in real code it might turn out that there is a null
in our Maybe. The fun part is that we don't have to manually check for that case. map
will do it for us:
const numberMaybe = Maybe.just(null);
const stringMaybe = numberMaybe.map(numberToString); // this does not crash!
Because null value is handled in the map
method itself, we really don't have to think anymore if there really is a value inside our Maybe. We can do operations on that "maybe value" without any checks and ifs.
Compare this with a typical use of a null
value, where - before any operation - we have to check if a value is really there:
const numberOrNull = /* this is either a number or null, we don't know */;
const stringOrNull = numberOrNull === null ?
null :
numberToString(numberOrNull);
Those checks are incredibly awkward, especially when such a value is used in many places in the codebase. Maybe allows you do this check only once - inside a map method - and then not think about it ever again.
And note once more just how similar this API is to our previous instances of a functor:
const arrayOfStrings = arrayOfNumbers.map(numberToString);
const promiseWithString = promiseWithNumber.then(numberToString);
const objectWithStrings = mapObject(objectWithNumbers, numberToString);
const maybeString = maybeNumber.map(numberToString);
Even though Maybe is something that works completely different from an array or a Promise, we can program with all those data structures using the same mental model.
Note also that all of our functor instances have some kind of corner case handling built in:
map
for arrays deals with the case of an empty array. mapObject
deals with empty objects. Promise.then
deals with Promises that were rejected. Maybe.map
deals with a null
value.
So not only we get a common API for multitude of data structures, we also get corner cases handled for us, so that we don't have to think about them anymore. How cool is that?
It is surprising that we achieved so much capabilities with such a simple concept - "a thing that can be mapped". It shouldn't be surprising that more complex interfaces in functional programming (like monads for example) are even more powerful and give even more benefits.
But that's a story for another article...
Functor laws
If you've already read about functors or monads before, you might have noticed that we omitted something. Monads (and functors as well) have famously some "laws" associated with them.
They resemble mathematical laws and are also something that successfully scares away people from learning functional programming. After all we just want to code, not do maths!
But it's important to understand that those laws are simply an equivalent of saying "this data structures is written in a reasonable way". Or, in another words, "this data structure is not stupid".
Let's see an example.
The first law for functors (there are two) states that if we take an identity function (which is just a function that returns it's argument):
const identity = a => a;
and we put it inside a map
method, this method will then return our data structure unchanged. Or rather it will return a new data structure, but with exactly the same shape as the previous one.
Indeed, if we call array's map with an identity, we will just get the same array again:
[1, 2, 3].map(identity) // this returns [1, 2, 3] again
But what if creators of JavaScript wanted to make the language a little bit more interesting and decided that map
would return values in... reverse order?
For example this code:
[1, 2, 3].map(numberToString)
would return ["3", "2", "1"]
array.
Then clearly:
[1, 2, 3].map(identity)
would return a [3, 2, 1]
array. But this is not the same array anymore! We have failed the first functor law!
So you can see that this law simply doesn't allow people to write dumb map
functions!
This is also the case with the second law, which states that mapping two functions one after the other:
someFunctor
.map(firstFunction)
.map(secondFunction)
should result in the same value as running those two functions once inside a map:
someFunctor.map(value => {
const x = firstFunction(value);
return secondFunction(x);
});
As an exercise, try to check if our reverse map
satisfies this condition or not.
Don't think about the laws TOO much
I have seen plenty of articles like "Promise is not actually a monad" etc.
And indeed those articles have some merit, but I believe that you shouldn't think about functor or monad laws too much. After all, as I've shown, they are here to simply ensure that a data structure is not written in an absurd way.
But if a data structure doesn't fully satisfy functor or monad laws, I still believe that it is valuable to think about it as a functor or monad.
That's because in day to day programming what is the most valuable is a functor as a design pattern (or interface), not as a mathematical concept. We are not trying to write here some academic code and then mathematically prove it's correctness. We are just trying to write code that is a bit more robust and more pleasant to read. That's all.
So even though - for example - a Promise might not really be a monad, I still think it is a great example of a monad, because it presents how "monadic" style might be used to deal with asynchronicity in an elegant manner.
So don't be a math geek. Stay pragmatic. :)
Conclusion
I hope that at this point a functor is not a mysterious concept for you anymore.
Which means you are ready to learn about monads! After understanding functors, learning monads is really just about making some changes to our design of a functor.
Leave me a comment if you would like to see a monad tutorial in a style similar to this article.
Also, if you enjoyed reading the article, subscribe to me on Twitter for more content on JavaScript, React and functional programming.
Thanks for reading and have a great day!
(Cover Photo by Nikola Johnny Mirkovic on Unsplash)
Posted on October 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.