Arrow Choice

API Documentation: Choice

Usually we deal with function more often, we're so familiar with A => B.

If we have two functions A => C and B => C, how can we compose them into a single function that can take either A or B and produce a C?

So basically we just look for a function that has type (A => C) => (B => C) => (Either[A, B] => C).

This is exactly typeclass Choice provided, if we make => more generic such as F[_,_], you will get a Choice

trait Choice[F[_, _]] {
  def choice[A, B, C](fac: F[A, C], fbc: F[B, C]): F[Either[A, B], C]

Note the infix notation of choice is |||.


A very useful case of Choice is middleware in HTTP server.

Take Http4s for example:

HttpRoutes[F] in Http4s is defined as Kleisli

type HttpRoutes[F[_]] = Kleisli[OptionT[F, *], Request[F], Response[F]]
// defined type HttpRoutes
def routes[F[_]]: HttpRoutes[F] = ???
// defined function routes

If we like to have an authentication middleware that composes the route, we can simply define middleware as:

type Middleware[F[_]] = Kleisli[OptionT[F, *], Request[F], Either[Response[F], Request[F]]]
// defined type Middleware
def auth[F[_]]: Middleware[F] = ???
// defined function auth

Which means the Request[F] goes through the middleware, will become option of Either[Response[F], Request[F]], where Left means the request is denied and return immediately, Right means the authentication is OK and request will get pass.

Now we need to define what we should do when middleware returns Left:

def reject[F[_]:Monad]: Kleisli[OptionT[F, *], Response[F], Response[F]] = Kleisli.ask[OptionT[F, *], Response[F]]
// defined function reject

Now compose middleware with route

def authedRoute[F[_]:Monad] = auth[F] andThen (reject[F] ||| routes[F])
// defined function authedRoute

You will then get a new route that has authentication ability by composing Kleisli.

HTTP Response

Another example will be HTTP response handler.

val resp: IO[Either[Throwable, String]] = httpClient.expect[String](uri"").attempt

attempt is syntax from MonadError

When we need to handle error, without Choice the handler would be something like:

  case Left => ???
  case Right => ???

With Choice there will be more composable solution without embedded logic in pattern matching:

def recover[A](error: Throwable): IO[A] = ???
def processResp[A](resp: String): IO[A] = ???

resp >>= (recover _ ||| processResp _)


ArrowChoice is an extended version of Choice, which has one more method choose, with syntax +++

trait ArrowChoice[F[_, _]] extends Choice[F] {
  def choose[A, B, C, D](f: F[A, C])(g: F[B, D]): F[Either[A, B], Either[C, D]]

With the middleware example, you can think ArrowChoice is middleware of middleware.

For example if we want to append log to middleware of auth, that can log both when rejected response and pass request:

def logReject[F[_]]: Kleisli[OptionT[F, *], Response[F], Response[F]] = ???
// defined function logReject
def logThrough[F[_]]: Kleisli[OptionT[F, *], Request[F], Request[F]] = ???
// defined function logThrough

See how easy to compose log functionality into our authedRoute:

def authedRoute[F[_]:Monad] = auth[F] andThen (logReject[F] +++ logThrough[F]) andThen (reject[F] ||| routes[F])
// defined function authedRoute