Async/Await for the Monadic Programmer

Inner Product LLC @InnerProductLLC

ScalaDays Seattle, 6 Jun 2023

inner product logo with url 320

Welcome to Seattle!

space needle

Remember the Chinese spy balloon?

What do I do?

I’m a Principal at Inner Product LLC, along with my partner Noel Welsh.

We’re consultants who write code,
train and mentor your staff.

We also write books.

We’ve both used Scala for over 10 years—​we’ve seen a lot!

inner product logo with url 320

arosien detail NoelWelshSq

Styles: monadic vs. direct

all the shirts

My original dilemma

public async Task<ActionResult<bool>> GetRedacted(...) {
  var user = HttpContext.User as RedactedPrincipal;
  if (...) return BadRequest(...);

  var results = ...;
  if (!results.IsValid) return BadRequest(results.ToString());

  try {
    Responses<RedactedResponse> redacted = await _redacted.Redacted(...);

    if (metadata ?? false) {
      return Ok(...);
    }
    else { return Ok(...); }
  }
  catch (ArgumentException e) {
    return BadRequest(e.ToString());
  }
}

☜ this is C#

This direct style mixes:

  • asynchronous execution

  • conditional logic / branching

  • early returns

into a (mostly) linear sequence.

How do I translate (functional, monadic(?)) Scala into this style?

  • asynchronous execution

  • conditional logic

  • early returns

thanksihateit

☜ Future, IO…​
☜? hopefully some cats combinators
☜? monads

But why is this translation so complicated?

That is, why is it bugging me so much??

Outline

  1. Using monads

  2. Using async/await

  3. Reconciling these styles

Using Monads

flatmap cat
for {

values effects

} yield expr

* All effects must have the same type; e.g., all Option, all Future, etc.

"happy-path" programming

Monads abstract over errors (usually). This is (usually) a good thing.

def doSomething(req: Request): F[Response] =
  for {
    a <- serviceA(toARequest(req))
    b <- serviceB(toBRequest(a))
  } yield toResponse(a, b)

What does this really do?

def doSomething(req: Request): F[Response] =
  for {
    a <- serviceA(...)
    b <- serviceB(...)
  } yield toResponse(a, b)
def doSomething(req: Request): F[Response] =

  serviceA(...).flatMap { a =>
    serviceB(...).map { b =>
      toResponse(a, b)
    }
  }

Tip: introduce local values via =

def doSomething(req: Request): F[Response] =
  for {
    a <- serviceA(toARequest(req))
    rb = toBRequest(a)
    b <- serviceB(rb)
    res = toResponse(a, b)
  } yield res

Fail-fast

def doSomething(req: Request): F[Response] =
  for {
    a <- serviceA(toARequest(req)) 🧨
    b <- serviceB(toBRequest(a))
  } yield toResponse(a, b)

  • The call to serviceA fails.

  • No a is produced, so no call to serviceB, etc. Everything stops.

  • doSomething returns the error.

Fail-fast

Monadsuccess typeerror type

Option[A]

A

Unit

Either[E, A]

A

E

Try[A]

A

Throwable

Future[A]

A

Throwable

IO[A]

A

Throwable

…​

…​

…​

Error handling

def doSomething(req: Request): F[Response] =
  for {
    a <- serviceA(toARequest(req))     🧨
           .handleError(_ => defaultA) πŸ™ˆ
    b <- serviceB(toBRequest(a))
  } yield toResponse(a, b)

Note: we don’t use built-in conditional logic (if/else) with monads. They need to be into the monad, usually as combinators.

See also: MonadError typeclass for error-handling combinators.

Early return

def doSomething(req: Request): F[Response] =
  serviceA(toARequest(req))               πŸ‘
    .ensure(new RuntimeException("meh!")) 🧨
      (a => a.isValid)                    🧐
    .handleError(_ => defaultResponse)    ⏏️
    .flatMap {                            πŸ₯³
      for {
        b <- serviceB(toBRequest(a))
      } yield toResponse(a, b)
    }

I don’t actually know a good way to do this monadically. This is my best guess.

See also: MonadError typeclass for error-handling combinators.

Critique

  • People struggle with for-comprehensions. It’s a wholly different syntax.

  • On the other hand, perhaps they are conceptually simpler? Less syntax: no val keyword required, etc.

  • Conditionals, error handling and early return are all handled as combinators—​that you need to know—​on the monad. These use typeclasses and other more advanced patterns.

Using Monads

m is for monad tutorials
  • Monads abstract over the sequencing of effects: what happens next.

  • Monads fail-fast. We can also handle errors using monad-specific or monad-agnostic combinators (via Typelevel cats, etc.).

  • for-comprehensions provide a different + better syntax for monadic sequencing. But it is different than using vals and normal function calls, and so requires more training.

Using Async/Await

mechanical keyboard layout

Async/Await example

val io: IO[Int] = ???

val program: IO[Int] =
  async[IO] {           (1)
    io.await + io.await (2)
  }
1The async "capability" creates a scope where waiting may occur.
2We can await effects and act on their results as regular values.

This is using the Typelevel Cats Effect async/await support.

What does this really do?

val io: IO[Int] = ???

val program: IO[Int] =
  async[IO] {
    io.await + io.await
  }
val io: IO[Int] = ???

val program: IO[Int] =
  io.flatMap { x =>
    io.flatMap { y =>
      IO.pure(x + y)
    }

Other systems and libraries create something similar.

Error handling

def doSomething(req: Request): F[Response] =
  async {
    val a = await(serviceA(toARequest(req))) 🧨
              .?(defaultResponse)            ⏏️
    val b = await(serviceB(toBRequest(a)))   πŸ₯³

    toResponse(a, b)
  }

There are proposals to add a early return operator (? in this example) which can optionally supply a default value in case of an error.

Early return (alternative)

def doSomething(req: Request): F[Response] =
  async {
    val a = await(serviceA(toARequest(req)))

    if (!a.isValid) defaultResponse
    else {
      val b = await(serviceB(toBRequest(a)))

      toResponse(a, b)
    }
  }

This does validation and early return.

Critique

  • Async/await is great for programmers who are new to concurrent programming.

  • But it is non-referentially-transparent. This really bugs functional programmers; refactoring is not safe

  • It has limited support for higher-order functions—​like map or traverse--because it’s a macro.

Using Async/Await

e is for effect
  • It’s new in the Scala ecosystem, but can be a good on-ramp for those new to the language.

  • At the same time, programmers can use the most common parts of the languge (keywords like if, else, etc.) and only need to learn a very small set of new features in order to use asynchronous programming.

Reconciling these styles

red pandas

Back to my grumbling…​

I was looking for some theory to explain
my mental mismatch when translating
between monadic and direct styles.

control structures

linear

tree-like

lists

algebraic data types

statements

expressions

imperative

functional

Structure

linear

tree-like

Data structures

lists (iterables?)

algebraic data types

Unit of expression

statements

expressions

Paradigm

imperative

functional

Branching

if/else

MonadError typeclass

Error Handling

try/catch, ?-like capability methods

MonadError typeclass

These start to mix and match.

A path forward

Async/await is fantastic for pure → effectful refactoring.

+ async {
    ...

-   if (isFeatureFlag(FOO)) somethingLocal()
+   if (isFeatureFlag(FOO)) await(somethingRemote())
    else somethingElse()

    ...
+ }

You don’t have to change the (general) structure of the code.

Source: Li Haoyi

A path forward

Async/await is fantastic for pure → effectful refactoring.

You can then use the more full-featured monadic style in subsequent refactorings.

Summary

programming langs vs needs

Async/await vs. monads is not a technological choice. It’s about programmers' needs.

And different programmers have different needs.

Multiple styles can coexist, and we can work on providing guidance on how to do reduce the friction.

References

async/await:

Extra thanks to:

Async/Await for the Monadic Programmer

Adam Rosien @arosien / @arosien@fosstodon.org
Inner Product LLC @InnerProductLLC

Hire us to help with your projects, or train your staff!

essential effects check out my book ☞
essentialeffects.dev

inner product logo with url 320