Behind many of the platitudes on the front end are classic computer science basics. Today, as long as you have made web requests using JS, you have basically used Monad in functional programming. What’s going on here? Let’s start with callback hell…

Call back to Hell with Promise

If you are familiar with JS, you will be familiar with the callback function, which is the most common way to handle asynchronous events in this language. However, as we are well aware, workflows that process multiple asynchronous tasks sequentially can easily result in nested callbacks, making code difficult to maintain:

$.get(a, (b) => {
  $.get(b, (c) => {
    $.get(c, (d) => {
      console.log(d)
    })
  })
})
Copy the code

This problem has been plaguing JSer for a long time, and the community’s solutions have blossomed. One of the schemes that has become standard is called promises, where you can package asynchronous callbacks in promises and chain them together to work asynchronously with the promise.then method:

const getB = a= >
  new Promise((resolve, reject) = > $.get(a, resolve))

const getC = b= >
  new Promise((resolve, reject) = > $.get(b, resolve))

const getD = c= >
  new Promise((resolve, reject) = > $.get(c, resolve))

getB(a)
  .then(getC)
  .then(getD)
  .then(console.log)
Copy the code

While ES7 already has a more concise async/await syntax, promises are already very widely used. For example, fetch, the new standard for web requests, wraps the returned content as a Promise, as does axios, the most popular Ajax library. As for jQuery, the old foundation library that once occupied 70% of the web, Promise has been supported since version 1.5. This means that as long as you’ve made a web request on the front end, you’ve basically dealt with Promises. And Promise itself is a Monad.

However, much of the talk about Promise has focused on its various state transitions and API uses, which are a far cry from what Monad might sound like. How are the two concepts related? To do that, we have to at least understand what a Monad is.

Winter Sprite and Monad

Many students who might otherwise have been interested in learning functional languages such as Haskell might have been put off by the famous phrase Monad is a monoid ona functor. In fact, there is no difference between this sentence and the white scholar said “Dong Ma Xiaosan, xue CAI Bi Chi”, but it is a correct nonsense, after listening to understand people still understand, do not understand people still do not understand. So if you ever introduce Monad like this again, feel free to shoot him – hey wait, who said Winter Sprite was right!

Anyway, what is Monad? Instead of resorting to PLT or Haskell, consider this in the context of JS: Since Promise is an object in JS, similarly, you can think of Monad as a special object.

Since it is an object, its dark magic is no more than in the properties and methods of two places. Here’s a crucial question to answer: What special properties and methods does Monad have to help us escape callback hell?

We can clarify this with very simple pseudocode. If we have four things to do: A, B, C, and D, then based on callback nesting, you can write the simplest function expression like:

A(B(C(D)))
Copy the code

See the nightmare of nested callback? However, we can simplify the scenario in a few ways. First, let’s simplify the problem to the most common callback nesting:

A(B)
Copy the code

Based on the idea of adding an intermediate layer and inversion of control, we can implement A simple intermediate object P with just A dozen lines of code, passing A and B separately to this object, thus splitting the callback:

P(A).then(B)
Copy the code

Now, we have wrapped A layer, and THE container P is the prototype of Promise! The basic mechanism for this unnesting of callbacks has been explained in my blog post looking at the Promise concept and implementation from source code, and the corresponding code implementation is not described here.

However, this solution is only applicable to scenarios where nesting occurs between A and B functions. If you’ve tried implementing this version of P, you’ll see that we don’t have the ability to:

P(A).then(B).then(C).then(D)
Copy the code

Or the ability to:

P(P(P(A))).then(B)
Copy the code

This is where Monad comes in! Let’s start with the answer: Monad objects are an enhancement of this crude version of P, whose THEN can support this nesting and chain-calling scenario. Of course, that’s not the name of the API in Monad, but for reference, let’s take A look at one key detail in the Promise/A+ specification:

Each time we Resolve a Promise, we need to determine two situations:

  1. If the content of Resolve is still a Promise (calledthenable), thenRecursive ResolveThis Promise.
  2. If the content to be resolved is not a Promise, then depending on the content (object, function, primitive type, etc.), gofulfillrejectThe current Promise.

Intuitively, this detail ensures that the following two calls are completely equivalent:

/ / 1
Promise.resolve(1).then(console.log)

/ / 1
Promise.resolve(
  Promise.resolve(
    Promise.resolve(
      Promise.resolve(1)
    )
  )
).then(console.log)
Copy the code

Does this nesting look familiar? This is essentially Monad’s core capability in Promise’s garb: for a container like P that holds something, we can recursively take it apart layer by layer and pull out the values that are inside. Once this capability is implemented, with a few tricks, we can implement the following elegant chain-call API:

Promise(A).then(B).then(C).then(D)
Copy the code

As an added bonus, we can handle it exactly the same regardless of whether the B, C, and D functions return a value that is executed synchronously or a Promise that is resolved asynchronously. Like this synchronous addition:

const add = x= > x + 1
Promise
  .resolve(0)
  .then(add)
  .then(add)
  .then(console.log)
/ / 2
Copy the code

And this slightly twisted asynchronous addition:

const add = x= >
  new Promise((resolve, reject) = > setTimeout((a)= > resolve(x + 1), 1000))

Promise
  .resolve(0)
  .then(add)
  .then(add)
  .then(console.log)
/ / 2
Copy the code

Synchronous and asynchronous, they call exactly the same way as the end result!

To conclude, let’s take a look at what functional programming concepts lay behind the journey from callback hell to Promise.

  • The most simpleP(A).then(B)In the implementation, itsP(A)Equivalent to MonadunitInterface, capable ofWrap any values in a Monad container.
  • Support nested Promise implementations in itsthenWhat’s behind it is actually in FPjoinThe concept,Recursively unpack the inner container while the container is still inside, returning the value of the bottom container.
  • The chain call behind Promise is actually in MonadbindConcept. You can connect a bunch flat.then()Promise can help you smooth out the difference between synchronous and asynchronousApply one by one to the values in the container.

Going back to the original question in this video, what is Monad? An object is considered Monad as long as it has the following two methods:

  1. The ability to wrap a value as a container-In FP this is calledunit.
  2. The ability to apply a function to a value that is contained in a container– The difficulty here is that containers can be nested within containers, so applying functions to values requires recursion. In FP this is calledbind(This is the same as in JSbindCompletely different concepts, please do not confuse).

As we’ve already seen, promise.resolve () can wrap any value into A Promise, whereas the resolve algorithm in the Promise/A+ specification actually implements bind. Therefore, we can think of a Promise as a Monad. In fact, this is not a novel conclusion, Github has been proven from the perspective of code, interested students can feel 🙂

To conclude, consider this question: How do we connect Promise to Monad? The key to Promise’s elimination of callback hell is this:

  1. Break upA(B)P(A).then(B)In the form. This is actually what Monad uses to build containersunit.
  2. You can write both synchronous and asynchronousP(A).then(B).then(C)...This is actually from Monadbind.

Here, we can understand the role of Monad from the function of Promise, and use Monad concept to explain the design of Promise 😉

What is a monoid on a self-functor

At this point, once you understand Promise, you should be able to understand Monad. But what about Monad’s monoid? In fact, as long as you have read this, you have already seen autofunctors and monids.

Since the functor

A Functor, as it is called, is a container that holds values and transforms the contents of the container by passing in functions: in a simplified sense, the Promise. Resolve is a mapping that can put any value into a Promise container. Functors, which map categories to themselves, can correspond to Promise(A). Then () and still return the Promise itself.

MAO semigroup

Monadic, which is called Monadic, satisfies two conditions: the identity element and the associative law.

The identity element has two conditions like this:

First, f applied to unit(a) gives the same result as f(a) :

const value = 6
const f = x= > Promise.resolve(x + 6)

// The following two values are equal
const left = Promise.resolve(value).then(f)
const right = f(value)
Copy the code

Second, unit applied to a non-unit element m results in m itself:

const value = 6

// The following two values are equal
const left = Promise.resolve(value)
const right = Promise.resolve(value).then(x= > Promise.resolve(x))
Copy the code

The associative law is the condition that a • b • c is equal to a • b • c:

const f = a= > Promise.resolve(a * a)
const g = a= > Promise.resolve(a - 6)

const m = Promise.resolve(7)

// The following two values are equal
const left = m.then(f).then(g)
const right = m.then(x= > f(x).then(g))
Copy the code

The above few lines of code, in fact, is a proof of [Promise is Monad]. Here, we can find that the daily docking interface to write promises, we can write things can be first promoted to the Monad level of functional programming, and then use abstract algebra and category theory to explain, is the force case instantly improved XD

conclusion

All of the above arguments do not include Haskell content. We can use JS to explain what Monad is and what it does. To some extent, THE author agrees with Wang Yin’s point of view: the threshold of functional programming has been artificially raised or mythologized. Although it is very practical and easy to understand in actual development, it is difficult to use a set of concepts to formally define and explain, which is probably not conducive to the popularization of excellent tools and concepts.

Of course, just to be tough, the next time someone asks you what Promise is, reply with this:

Isn’t Promise just a monoid on a functor? What’s so hard to understand about 🙂

Finally, an AD: I was motivated to write this article by some new understanding of Promises while implementing Bumpover, a fully promise-enabled asynchronous data-conversion wheel. If you are interested, please pay attention to XD