In the JS world, we all know the devil, maybe not so terrible, are we more misunderstanding?

Into callback hell

I won’t dig too deep into the term callback hell, but will simply explain some of the problems and typical solutions through this article. If you’re not familiar with the term, read other articles first. I’ll be right here waiting for you! Ok, LET me copy and paste the problem code first, and then let’s solve it with callback functions instead of promise/async/await.

const verifyUser = function(username, password, callback) {
  dataBase.verifyUser(username, password, (error, userInfo) => {
    if (error) {
      callback(error);
    } else {
      dataBase.getRoles(username, (error, roles) => {
        if (error) {
          callback(error);
        } else {
          dataBase.logAccess(username, error => {
            if (error) {
              callback(error);
            } else{ callback(null, userInfo, roles); }}); }}); }}); };Copy the code

Crush the pyramids

If you look at the code, you’ll see that each time you need to perform an asynchronous operation, you must pass a callback function to receive the asynchronous result. Because we defined all callbacks linearly and anonymously, it became a bottom-up, dangerous pyramid of callbacks (in practice, the nesting could be more, deeper, and more complex). To start, let’s simply refactor the code by assigning each anonymous function to a separate variable. Introduce Curried aruguments to bypass variables in the environment scope.

const verifyUser = (username, password, callback) =>
  dataBase.verifyUser(username, password, f(username, callback));

const f = (username, callback) => (error, userInfo) => {
  if (error) {
    callback(error);
  } else{ dataBase.getRoles(username, g(username, userInfo, callback)); }}; const g = (username, userInfo, callback) => (error, roles) => {if (error) {
    callback(error);
  } else{ dataBase.logAccess(username, h(userInfo, roles, callback)); }}; const h = (userInfo, roles, callback) => (error, _) => {if (error) {
    callback(error);
  } else{ callback(null, userInfo, roles); }};Copy the code

If nothing else, it must be a little flattering. But this code still has the following problems:

  1. if (error) { ... } else { ... }Pattern reuse;
  2. Variable names mean nothing to logic;
  3. verifyUser,f,gandhThey are highly coupled because they refer to each other.

Look at the pattern

Before we tackle any of these issues, let’s note some similarities between these expressions: all of these functions accept some data and callback arguments. F, g and h take an additional pair of arguments (error, something), only one of which will be a non-null/undefined value. If error is not null, the function is immediately thrown to the callback and terminates. Otherwise, something is executed to do more work, eventually causing the callback to receive a different error, or null, and some result value. With these commonalities in mind, we’ll start refactoring intermediate expressions to make them look more and more similar.

Magic makeup!!

I find the if statement cumbersome, so let’s take the time to replace it with a ternary expression. The following code does not behave because the return value is discarded.

const f = (username, callback) => (error, userInfo) =>
  error
    ? callback(error)
    : dataBase.getRoles(username, g(username, userInfo, callback));

const g = (username, userInfo, callback) => (error, roles) =>
  error
    ? callback(error)
    : dataBase.logAccess(username, h(userInfo, roles, callback));

const h = (userInfo, roles, callback) => (error, _) =>
  error ? callback(error) : callback(null, userInfo, roles);
Copy the code

Currie,

Since we’re about to start doing some serious things with function arguments, I’ll take the opportunity to curryize the function as much as possible. We cannot curryize (error,xyz) arguments because databeseAPI expects callback functions to take two arguments, but we can curryize other arguments. We will use the following Corrification wrappers around the dataBaseAPI later:

const dbVerifyUser = username => password => callback =>
  dataBase.verifyUser(username, password, callback);

const dbGetRoles = username => callback =>
  dataBase.getRoles(username, callback);

const dbLogAccess = username => callback =>
  dataBase.logAccess(username, callback);
Copy the code

In addition, we replace callback(null, userInfo, roles) with callback(NULL, {userInfo, roles}) so that we can only handle one argument except for the inevitable error argument.

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)(f(username)(callback));

const f = username => callback => (error, userInfo) =>
  error
    ? callback(error)
    : dbGetRoles(username)(g(username)(userInfo)(callback));

const g = username => userInfo => callback => (error, roles) =>
  error ? callback(error) : dbLogAccess(username)(h(userInfo)(roles)(callback));

const h = userInfo => roles => callback => (error, _) =>
  error ? callback(error) : callback(null, { userInfo, roles });
Copy the code

Turn it out

Let’s do some more refactoring. We’ll pull all the error-checking code “out” one level, and the code will temporarily become clear. Instead of each step doing its own error checking, and forwarding the result and callback to the next step if there is no problem, we will use an anonymous function that receives errors or results for the current step:

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)((error, userInfo) =>
    error ? callback(error) : f(username)(callback)(userInfo)
  );

const f = username => callback => userInfo =>
  dbGetRoles(username)((error, roles) =>
    error ? callback(error) : g(username)(userInfo)(callback)(roles)
  );

const g = username => userInfo => callback => roles =>
  dbLogAccess(username)((error, _) =>
    error ? callback(error) : h(userInfo)(roles)(callback)
  );

const h = userInfo => roles => callback => callback(null, { userInfo, roles });
Copy the code

Notice how error handling completely disappears from our final function: h. It just takes a few arguments and immediately enters them into the callback it receives. The callback argument is now passed in various places, so for consistency we will move the argument so that all data appears first and the callback appears last:

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)((error, userInfo) =>
    error ? callback(error) : f(username)(userInfo)(callback)
  );

const f = username => userInfo => callback =>
  dbGetRoles(username)((error, roles) =>
    error ? callback(error) : g(username)(userInfo)(roles)(callback)
  );

const g = username => userInfo => roles => callback =>
  dbLogAccess(username)((error, _) =>
    error ? callback(error) : h(userInfo)(roles)(callback)
  );

const h = userInfo => roles => callback => callback(null, { userInfo, roles });
Copy the code

An evolving pattern

By now, you may have started to see patterns in the chaos. In particular, the code that callback computations for error checking and thread handling is very repetitive and can be broken down using the following two functions:

const after = task => next => callback =>
  task((error, v) => (error ? callback(error) : next(v)(callback)));

const succeed = v => callback => callback(null, v);
Copy the code

Our steps become:

const verifyUser = username => password =>
  after(dbVerifyUser(username)(password))(f(username));

const f = username => userInfo =>
  after(dbGetRoles(username))(g(username)(userInfo));

const g = username => userInfo => roles =>
  after(dbLogAccess(username))(_ => h(userInfo)(roles));

const h = userInfo => roles => succeed({ userInfo, roles });
Copy the code

It’s time to pause and try to concatenate after and suceed into these new expressions. These new expressions do equate to the factors we consider. OK, let’s see, f, G and H seem to be useless.

So let’s get rid of them! All we have to do is go backwards from h and inline each function into the definition that references it:

Const g = username => userInfo => roles => after(dbLogAccess(username))(_ => succeed({userInfo, roles }));Copy the code
// line g to f const f => userInfo => after(roles => after(dbLogAccess(username))(_ => succeed({ userInfo, roles })) );Copy the code
Const verifyUser = username => password => after(dbVerifyUser(username)(password))(userInfo => after(dbGetRoles(username))(roles => after(dbLogAccess(username))(_ => succeed({ userInfo, roles })) ) );Copy the code

We can use reference transparency to introduce some temporary variables and make them more readable:

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  return after(userVerification)(userInfo =>
    after(rolesRetrieval)(roles =>
      after(logEntry)(_ => succeed({ userInfo, roles }))
    )
  );
};
Copy the code

Now you’ve got it! It’s fairly compact, without any repetitive error checking, and even somewhat similar to the Promise pattern. You would call verifyUser like this:

const main = verifyUser("someusername") ("somepassword");
main((e, o) => (e ? console.error(e) : console.log(o)));
Copy the code

The final code

// APIs const after = task => next => callback => task((error, v) => (error? callback(error) : next(v)(callback))); const succeed = v => callback => callback(null, v); Const dbVerifyUser = username => password => callback => database. verifyUser(username, password, callback); callback); const dbGetRoles = username => callback => dataBase.getRoles(username, callback); const dbLogAccess = username => callback => dataBase.logAccess(username, callback); // Result const verifyUser = username => password => {const userVerification = dbVerifyUser(username)(password); const rolesRetrieval = dbGetRoles(username); constlogEntry = dbLogAccess(username);

  return after(userVerification)(userInfo =>
    after(rolesRetrieval)(roles =>
      after(logEntry)(_ => succeed({ userInfo, roles }))
    )
  );
};
Copy the code

The ultimate magic

Are we done? Some may still find verifyUser’s definition a little too triangulated. There’s a way around it, but first let’s do something else. I have not independently found that after and succeed procedures are defined when refactoring this code. I actually pre-defined these definitions because I copied them from the Haskell library with names >>= and Pure. Together, these two functions form the definition of a continuation monad. Let’s format and define verifyUser in a different way:

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  // prettier-ignore
  return after   (userVerification)    (userInfo =>
         after   (rolesRetrieval)      (roles    =>
         after   (logEntry)            (_        =>
         succeed ({ userInfo, roles }) )));
};
Copy the code

Replace succeed and After with those strange aliases:

const M = { "> > =": after, pure: succeed };

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  return M["> > ="] (userVerification)    (userInfo =>
         M["> > ="] (rolesRetrieval)      (roles    =>
         M["> > ="] (logEntry)            (_        =>
         M.pure   ({ userInfo, roles }) )));
};
Copy the code

M is our definition of “Continuation monad”, with its error-handling and impure side effects. Details are omitted here to prevent the article from being twice as long, but the relevance is that there are many convenient ways to sort “continuation monad” calculations that are not affected by the doomsday effect of the pyramid. Without further explanation, there are several ways to express verifyUser:

const { mdo } = require("@masaeedu/do");

const verifyUser = username => password =>
  mdo(M)(({ userInfo, roles }) => [
    [userInfo, () => dbVerifyUser(username)(password)],
    [roles, () => dbGetRoles(username)],
    () => dbLogAccess(username),
    () => M.pure({ userInfo, roles })
  ]);
Copy the code
// Apply promotion const verifyUser = username => password => m.lift (userInfo => roles => _ => ({userInfo, roles }))([ dbVerifyUser(username)(password), dbGetRoles(username), dbLogAccess(username) ]);Copy the code

I have deliberately avoided introducing concepts like type signatures or monad for most of this article to make things approachable. Perhaps in future posts we can re-derive this abstraction with the monad and Monad-Transformer concepts that are most important to our minds, paying particular attention to types and patterns.

Thank you

Many thanks to @Jlavelle, @Mvaldesdeleon and @GabeJohnson for their feedback and suggestions on this post.