DEV Community

Sal Rahman
Sal Rahman

Posted on

Functors, Monads, and Promises

The JavaScript Promise is a tool for asynchronous operation. However, it is a whole lot more powerful than that.

The promise's then method can be thought to act both like map and flatMap.

Arrays, map, flatMap, Functors, and Monads

Recall that in JavaScript arrays, map allows you to take an array, and get a totally new array, with each elements entirely transformed. In other words, map takes an array (implicitly), a function, and returns another array.

So, for instance, if you wanted to derive an array of strings from an array of numbers, you would invoke the map method, by supplying a function.

Here's an example.

const nums = [ 1, 2, 3, 4, 5 ];

const strs = nums.map(n => n.toString());

// Should be:
// [ '1', '2', '3', '4', '5' ]
Enter fullscreen mode Exit fullscreen mode

Because arrays implement a map method, you can think of arrays as functors.

Arrays also implement a flatMap method. Like map, it is also used to derive an entirely new array. But the key difference here is that rather than the supplied function returning the transformed value, it can return it wrapped inside an array.

const nums = [ 1, 2, 3, 4, 5 ];

const strs = nums.flatMap(n => [ n.toString() ]);
// Note: we're returning an ARRAY with a single string!

// Should be:
// [ '1', '2', '3', '4', '5' ]
Enter fullscreen mode Exit fullscreen mode

In case your wondering: yes, the returned array can absolutely have more than one element in it. Those values will simply be concatenated into the final result.

Because arrays implement flatMap, you can think of arrays as Monads.

About Functors and Monads

Functors and monads are two constructs that hold value.

Functors implement map, and monads implement flatMap.

Functors and monads can be defined to hold any number of values, whether it be strictly one, two, three, or unlimited.

Promises as Functors and Monads

The JavaScript promise represents a construct that holds a single value.

A promise's then method acts as both map, and flatMap.

The method then, like map, and flatMap, will always return a promise.

With then, you can have the function return a non-promise value. This will have then act like an array's map method. Or, you can have that function return a promise. This will have then act like an array's flatMap method.

Here is then acting like map.

promise.then((x) => {
  return x + 42;
});
Enter fullscreen mode Exit fullscreen mode

Here is then acting like flatMap.

promise.then((x) => {
  // Note: Promise.resolve will return a promise.
  return Promise.resolve(x + 42);
});
Enter fullscreen mode Exit fullscreen mode

Monad laws with promise

Monads have laws. Think of them like Newton's three laws of motion.

These are:

  • left-dentity
  • right-identity
  • associativity

Because promises can be interpreted as monads, you most certainly can use then to follow the three laws.

Let's demonstrate. First, let's assume that the functions f and g accept a value and returns a promise, and p is a promise.

Left-identity

Promise.resolve(x).then(f)

// Is equivalent to

f(x)
Enter fullscreen mode Exit fullscreen mode

Right-identity

p.then(Promise.resolve)

// Is equivalent to

p // I'm serious. that's all there is to it.
Enter fullscreen mode Exit fullscreen mode

Associativity

p.then(x => f(x).then(g))

// Is equivalent to

p.then(f).then(g)
Enter fullscreen mode Exit fullscreen mode

Monadic error handling in Promise

Traditionally flatMap (the then in promises) is very instance-specific. After all, you can substitute the name flatMap with whatever name you want, so long as the instance behaves like a monad. And in the case of promises, flatMap is called then.

Other than the name (then instead of flatMap), the way it is implemented can be different from instance to instance.

And in the case of Promises, it can be implemented so that then does not evaluate if the Promise holds no value other than an error.

For example

Promise.reject(new Error('Some error'))
  .then(() => {
    console.log('Wee!');
    // Trust me. Nothing will happen here.
  });
Enter fullscreen mode Exit fullscreen mode

In order to do anything with the promise, you will need to invoke the catch method. The catch method will return a promise, just like then.

However, while then will only evaluate the function if the promise holds a value, catch will evaluate the function if the promise holds an error.

Promise.reject(new Error('Some error'))
  .then(() => {
    console.log('Wee!');
    // Trust me. Nothing will happen here.

    return Promise.resolve(1);
  })
  .catch(() => {
    console.log('Caught an error!')
    return Promise.resolve(42);
  })
  .then(x => {
    console.log(x);
    // Will log 42, not 1.
  });
Enter fullscreen mode Exit fullscreen mode

Interestingly enough, the monad laws will also work with catch, as well as then.

Conclusion

So this article went over what a monad is, and how promises can be thought of as monads. To put it in simple terms, an object can be thought of as a monad, as long as it implements some method that looks like flatMap.

Latest comments (0)