Monad Context

Monad context is a way to provide an additional API, which is available only inside some monad (i.e., inside appropriative async block). In the introduction chapter, we have shown a simplified representation of the async signature:

def async[F[_], T](using am: CpsMonad[F])(expr: T) => F[T]

The complete definition looks like:

transparent inline def async[F[_]](using am: CpsMonad[F]) =
  macros.Async.InferAsyncArg(using am)

// and then in macros.Async:

class InferAsyncArg[F[_], C](using val am: CpsMonad.Aux[F, C]) {

  transparent inline def apply[T](inline expr: C ?=> T) =
    // ...

}

Here we split an application into two parts, to have one type parameter in async; this becomes possible with the async[F] syntax. Take a look at the argument of the InferAsyncArg.apply method: expr: C ?=> T. This is a context function. The context parameter C is extracted from the monad definition. Inside expr the Scala 3 compiler makes an implicit instance of C available, which we can use to provide an internal monad API.

The complete await signature looks like:

def await[F[_], T, G[_]](using CpsAwaitable[F], CpsMonadContext[G])(expr: T) => F[T]

where F is a type of awaited wrapper and G monad in enclosing async block.

Also, an instance of monad context is automatically available inside the direct context functions, i.e., when we have CpsDirect[F] in the current scope.

Using a context parameter makes our monad a bit more complex than traditional Haskell-like monad constructions, but allows us to represent important industry cases, like structured concurrency. Jokingly, we can say that our monad is close to the original Leibnic definition of monad in his work Monadology, where each monad has unique qualities, not accessible from outside.

The monad context is defined as a type inside CpsMonad :

trait CpsMonad[F[_]] ....

  type Context <: CpsMonadContext[F]
  // ...

}

CpsMonadContext provides the functionality to adopt awaiting another monadic expression into the current context.

trait CpsTryMonadContext[F[_]] {

  /**
   * Return monad, where operations are intercepted with current context.
   **/
  def monad: CpsTryMonad[F]

}

As a practical example, let’s consider adding a timeout to the plain Scala future. I.e., let’s think about how to build the monad FutureWithTimeout, which will complete within a timeout or fire a TimeoutException. It’s more or less clear how to combine a small Future with timeouts into one (at this point, we can rename timeouts to deadlines), but what should we do when the control flow is waiting for completing an external Future in await?

The answer is the usage of a monad context: intercepted monadic operation can generate a promise,

which will be filled in case of finishing of origin underlaying operation or elapsing timeout.

See example TestFutureWithDeadline.scala for the implementation of such an approach.

Note that this is one variant of the code organization approach. Alternatively, we can signal to f, if we know that we exclusively own f evaluation. This can be an approach for lazy effect. The design choice for possible solutions is quite large.

With direct context encoding, you can pass information from the top-level context into subsequential computations via monad conversion boundaries with a custom implementation of CpsMonadContextInclusion.

For monad writers: as a general design rule, use monad context when you want to provide access to some API, which should be visible only inside a monad (i.e. inside async or direct context function). For trivial cases, when you don’t need a context API, you can mix CpsMonadInstanceContext into your implementation of trait CpsMonad. For more advanced cases, we advise using the CpsContextMonad trait.

Also, you can notice the compatibility of this context with monadic-reflection, based on Flinsky encoding, where async becomes reify and await accordingly reflect.