DEV Community

Artur Muniz
Artur Muniz

Posted on • Edited on

Hacking JS async/await to chain Monads

The M word.

then :: Monad m => m a ~> (a -> m b) -> m b

await a minute

Native JS promises follow the https://promisesaplus.com/. A+ compliant promises implementation can interoperate, but it does so by being hopeful that anything that implements a then method will behave like a promise, and that's the point we'll be hacking in.

From the spec, the important parts for us are:

1.2 “thenable” is an object or function that defines a then method.

[...]

2.2 A promise’s then method accepts two arguments:
promise.then(onFulfilled, onRejected)

[...]

2.2.2 If onFulfilled is a function:
2.2.2.1 it must be called after promise is fulfilled, with promise’s value as its first argument.
2.2.2.2 it must not be called before promise is fulfilled.
2.2.2.3 it must not be called more than once.

And the most significant bit:

[...] If x is a thenable, attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.

All of that means that:
1 - We must implement a then method, to trick the The Promise Resolution Procedure into calling it. It will be a alias for the bind operation.
2 - As by 2.2.2.3, our then will be feed a onFulfilled function that expects only one call, i.e. chaining enumerations won't be possible.

Tricking JS

Consider the following monad:

const Const = (x) => ({
  then (onFulfilled) {
    return onFulfilled(x)
  }
})

const distopy = Const(1000)
  .then(x => Const(x + 900))
  .then(x => Const(x + 80))
  .then(x => Const(x + 4)) // Const(1984)

then's signature is: then :: Const a ~> (a -> Const b) -> Const b

Now, I want a function that given to Const number, returns a Const* with the sum of both. I just need to write something like:

function sumConsts (constA, constB) {
  return constA
    .then(a => constB
      .then(b => Const(a + b)
    )
  )
}

The more Consts we need to sum, the more it will look-like a callback-hell, so I'd take the advantage of Const being a thenable and refactor sumConsts as:

const sumConsts = async (constA, constB) => Const(await constA + await constB)

But now, as async functions always returns a promise into the returned value and Const is a thenable the promise resolution procedure will kick in, and make the returned promise "attempts to adopt the state of" it, so will never get a Const back, but as both Const and promises implement the same interface, the Const semantic is kept.

Maybe another example

const Maybe = {
 Just: (v) => {
   const typeofV = typeof v
   if (typeofV === 'undefined' || typeofV === 'null') {
     return Maybe.Nothing
   }

   return {
     then (onFulfilled) {
       return onFulfilled(v)
     }
   }
 },

 Nothing: {
   // You can either never call `onFulfilled`, so a Nothing never resolves.
   // then() {},

   // Or call `onRejected`, so resolving a Nothing rejects the promise
   then(onFulfilled, onRejected) {
     onRejected(Maybe.Nothing)
     return Maybe.Nothing
   }
 }
}

function flipCoin (myGuess) {
  const coin = Math.random() < 0.5 ? 'heads' : 'tails'
  if (coin === myGuess) {
    return Maybe.Just (myGuess)
  } else {
    return Maybe.Nothing
  }
}

async function playIt (guess = 'heads', tries = 1) {
  try {
    await flipCoin (guess)
    return tries
  } catch (reason) {
    if (reason === Maybe.Nothing)
      return playIt(guess, tries + 1)
    else
      throw reason
  }
}

playIt()
  .then(console.log) // avg output: 2

Top comments (0)