MaybeAsync and EitherAsync for Haskellers

Keep in mind a lot of stuff have changed since this was written (back in January 2019), Either and MaybeAsync evolved to be more distant than the monad transformer design in late 2020
and instead adopted the PromiseLike interface to make it easier to work with and reduce the large amount of boilerplate the original implementation required.

As mentioned in the description of those data types, MaybeAsync and EitherAsync are funky Promise-specialized monad transformers for Maybe and Either.
Some things may feel out of place and that is completely intentional, porting monad transformers over to TypeScript was just not practical, especially the higher-kinded types and typeclasses part.
A lot of thought went into designing the APIs and I believe that the result is satisfactory. In fact, even though the implementation is completely different, code written in mtl style looks pretty similar! Here, take a look:
tryToInsertUser user = runExceptT $ do
  validatedUser <- liftEither $ validateUser user
  userExists <- lift $ doesUserAlreadyExist validatedUser

  when userExists (throwE UserAlreadyExists)

  maybeToExceptT ServerError $ do
    updatedUser <- MaybeT $ hashPasswordInUser user
    lift $ insertUser updatedUser
Keep in mind this code is not representative of the perfect or cleanest implementation for such a feature, I tried to shove as much functions, that are also possible in Maybe-EitherAsync, as I could.
Here's the same logic implemented with purify in TypeScript:
const tryToInsertUser = user =>
  EitherAsync(async ({ liftEither, throwE, fromPromise }) => {
    const validatedUser = await liftEither(validateUser(user))
    const userExists = await doesUserAlreadyExist(validatedUser)

    if (userExists) throwE('UserAlreadyExists')

    return fromPromise(MaybeAsync(async ({ fromPromise }) => {
        const updatedUser = await fromPromise(hashPasswordInUser(user))
        return insertUser(updatedUser)
One important thing to understand about Maybe and EitherAsync is that the docs and the API create the illusion that code is running in some custom magical context that lets you safely unwrap values.
Is it referred to as "MaybeAsync context" or "EitherAsync context", but in fact there's no magic and the only real context is the async/await block.
That allows us to simulate do-notation using await and what those "lifting" function actually do is return Promises that get rejected when a value is missing.
The `run` function will later on catch all those rejections and return a proper Maybe/Either value.

Glossary of functions

  • MaybeAsync<a> = MaybeT IO a
  • EitherAsync<e, a> = ExceptT e IO a
  • liftEither/Maybe = liftEither/Maybe (MaybeT/ExceptT . return in Haskell, but nothing like that in purify, they function the same though)
  • fromPromise = the MaybeT/ExceptT constructor (you only need to wrap the IO action with the newtype in Haskell, in purify it's not as simple)
  • throwE = throwE
  • MaybeAsync#toEitherAsync = maybeToExceptT (from the transformers package)
  • EitherAsync#toMaybeAsync = exceptToMaybeT