Hacking JS async/await to chain Monads
Artur Muniz
Posted on July 23, 2019
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 athenable
, attempts to make promise adopt the state ofx
, under the assumption thatx
behaves at least somewhat like a promise. Otherwise, it fulfills promise with the valuex
.
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 Const
s 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 function
s 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
Posted on July 23, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.