The original link
The CertSimple website recently published an article saying that async and await in ES2017 are the best features of JS. I couldn’t agree more.
Basically, one of the few advantages of JS is that it handles asynchronous requests properly. This is thanks to the functions and closures it inherits from Scheme.
However, this is one of the biggest problems with JS because it leads to callback hell, a seemingly unavoidable problem that makes asynchronous JS code very unreadable. Many solutions have been tried and mostly failed to solve callback hell. The Promise scheme came close to solving this problem, but it failed.
Finally, we saw a scheme combining async/await with Promise that solved the problem very well. In this article I will explain why this is the case and the relationship between Promise, async/await and do syntax, and monad.
First, we tried three different styles of code to fetch and read the balances in all accounts of the user. (A user has more than one account accout, each account has a balance balence)
Wrong scheme: Callback hell
function getBalances(callback) {
api.getAccounts(function (err, accounts) { / / callback
if (err) {
callback(err);
} else {
var balances = {}; / / the balance
var balancesCount = 0;
accounts.forEach(function(account, i) {
api.getBalance(function (err, balance) { / / callback
if (err) {
callback(err);
} else {
balances[account] = balance;
if (++balancesCount === accounts.length) {
callback(null, balances); }}}); }); }}); };Copy the code
This is an easy way to think about it, but it has two layers of callbacks, and there are three problems with this ugly code that need to be addressed:
- Err is handled everywhere
- Use counters to calculate asynchronously obtained values
- Inevitable nesting
Almost right: Promise
function getBalances() {
return api.getAccounts()
.then(accounts= >
Promise.all(accounts.map(api.getBalance))
.then(balances= > Ramda.zipObject(accounts, balances))
);
}
Copy the code
This code addresses the above three problems:
- We can handle errors uniformly in the last THEN
- Promise.all eliminates the need to define additional counters
- Nesting can be avoided to the greatest extent possible
However, there is still a problem that then is still nested. The second THEN is in the callback of the first THEN, because the second THEN needs the accounts variable of the first then. So it’s important to indent your code correctly.
The first THEN passes accounts to the second then:
function getBalances() {
return api.getAccounts()
.then(accounts= > Promise.all(accounts.map(api.getBalance)
.then(balances= > [accounts, balances])))
.then(([accounts, balances]) = > Ramda.zipObject(accounts, balances));
}
Copy the code
But that leads to another “THEN”. You can see that Promise basically solved the callback below, but not completely.
Correct scheme: async/await
async function getBalances() {
const accounts = await api.getAccounts();
const balances = await Promise.all(accounts.map(api.getBalance));
return Ramda.zipObject(balances, accounts);
}
Copy the code
An async function can have an await keyword, and an await gets a Promise to complete the task before the next sentence is executed.
And with that we don’t have to indent the pain anymore. How does this work? We need to go back to the source.
Call back to the origins of Hell
Many people think of callback hell as something that only happens in asynchronous tasks, but it actually happens whenever we use callbacks to deal with wrapped values.
Suppose you want to print out [1, 2, 3] [4 and 6] [7,8,9] all the permutation and combination, such as,4,7 [1],4,8 [1], etc. :
[1.2.3].map((x) = >{[4.5.6].map((y) = >{[7.8.9].map((z) = > {
console.log(x,y,z); })})});Copy the code
Look, the familiar callback hell has emerged. This is completely synchronous code, but async and await can only handle async…
Assuming we create a similar keyword called multi/pick for the synchronous code, the above code can be written as
multi function () {
x = pick [1.2.3];
y = pick [4.5.6];
z = pick [7.8.9];
console.log(x, y, z);
}
Copy the code
Of course, this syntax doesn’t exist.
Monad and do
Some languages have features that can handle all of these requirements and make no distinction between asynchronous and synchronous.
The intermediate process requires some TS and Haskell knowledge, please read for yourself. The code looks something like this:
getBalances: :Promise (Map String String) This is the type declaration
getBalances = do
accounts <- getAccounts
balances <- getBalance accounts
return (Map.fromList (zip accounts balances))
Copy the code
This syntax is called the DO tag or do syntax. It requires promises to meet some of Monad’s rules.
The DO syntax and Monad were used in Haskell in 1995.
These two features have since solved callback hell. If you compare JS Promise, await/async with Haskell’s Monad, do syntax, you’ll see
Await /async is to Promise what do syntax is to Monad
Now that Haskell has proven that Monad can effectively avoid callback hell, JS can simply rely on await.
conclusion
JS is great again. But why did it take JS so long to borrow from Monad? If only the community had followed the advice of “that crazy guy” in 2013.
The full text.
What did that crazy guy say? Open the link and you’ll see a GitHub Issues page with the guy’s name Brian Mckenna.
Brian suggested using a functional programming approach to optimize Promise.
Domenic, the bill’s defender, was unsympathetic.
Was said
We’re not going to do that. This solution is impractical and creates strange and useless apis that can’t be used in JS to satisfy someone’s own aesthetic preferences. What you don’t understand is that the problem Promise addresses is providing an asynchronous flow control model in an imperative programming language. This scenario was hilariously inaccurate, as it failed to meet our specs and should only pass 1/500 of our test cases.
The response got 16 likes and 254 clicks.