Stacking Future and Either 

One use of monad transformers that seem to come up often is stacking Future datatype with Either. There’s a blog post by Yoshida-san (@xuwei_k) in Japanese called How to combine Future and Either nicely in Scala.

A little known fact about Yoshida-san outside of Tokyo, is that he majored in Chinese calligraphy. Apparently he spent his collge days writing ancient seal scripts and carving seals:

His namesake Xu Wei was a Ming-era painter, poet, writer and dramatist famed for his free-flowing style. Here’s Yoshida-san writing functional programming language.

In any case, why would one want to stack Future and Either together? The blog post explains like this:

  1. Future[A] comes up a lot in Scala.
  2. You don’t want to block future, so you end up with Future everywhere.
  3. Since Future is asynchronous, any errors need to be captured in there.
  4. Future is designed to handle Throwable, but that’s all you get.
  5. When the program becomes complex enough, you want to type your own error states.
  6. How can we make Future of Either?

Here’s the prepration step:

scala> :paste
// Entering paste mode (ctrl-D to finish)
case class User(id: Long, name: String)

// In actual code, probably more than 2 errors
sealed trait Error
object Error {
  final case class UserNotFound(userId: Long) extends Error
  final case class ConnectionError(message: String) extends Error
}
object UserRepo {
  def followers(userId: Long): Either[Error, List[User]] = ???
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

defined class User
defined trait Error
defined object Error
defined object UserRepo
import UserRepo.followers

Suppose our application allows the users to follow each other like Twitter. The followers function returns the list of followers.

Now let’s try writing a function that checks if two users follow each other.

scala> def isFriends0(user1: Long, user2: Long): Either[Error, Boolean] =
         for {
           a <- followers(user1).right
           b <- followers(user2).right
         } yield a.exists(_.id == user2) && b.exists(_.id == user1)
isFriends0: (user1: Long, user2: Long)Either[Error,Boolean]

Now suppose we want to make the database access async so we changed the followers to return a Future like this:

scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.concurrent.{ Future, ExecutionContext }
object UserRepo {
  def followers(userId: Long): Future[Either[Error, List[User]]] = ???
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

import scala.concurrent.{Future, ExecutionContext}
defined object UserRepo
import UserRepo.followers

Now, how would isFriends0 look like? Here’s one way of writing this:

scala> def isFriends1(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): Future[Either[Error, Boolean]] =
         for {
           a <- followers(user1)
           b <- followers(user2)
         } yield for {
           x <- a.right
           y <- b.right
         } yield x.exists(_.id == user2) && y.exists(_.id == user1)
isFriends1: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)scala.concurrent.Future[Either[Error,Boolean]]

And here’s another version:

scala> def isFriends2(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): Future[Either[Error, Boolean]] =
         followers(user1) flatMap {
           case Right(a) =>
             followers(user2) map {
               case Right(b) =>
                 Right(a.exists(_.id == user2) && b.exists(_.id == user1))
               case Left(e) =>
                 Left(e)
             }
           case Left(e) =>
             Future.successful(Left(e))
         }
isFriends2: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)scala.concurrent.Future[Either[Error,Boolean]]

What is the difference between the two versions? They’ll both behave the same if followers return normally, but suppose followers(user1) returns an Error state. isFriend1 would still call followers(user2), whereas isFriend2 would short-circuit and return the error.

Regardless, both functions became convoluted compared to the original. And it’s mostly a boilerplate to satisfy the types. I don’t want to imagine doing this for every functions that uses Future[Either[Error, A]].

EitherT datatype 

Cats comes with EitherT datatype, which is a monad transformer for Either.

/**
 * Transformer for `Either`, allowing the effect of an arbitrary type constructor `F` to be combined with the
 * fail-fast effect of `Either`.
 *
 * `EitherT[F, A, B]` wraps a value of type `F[Either[A, B]]`. An `F[C]` can be lifted in to `EitherT[F, A, C]` via `EitherT.right`,
 * and lifted in to a `EitherT[F, C, B]` via `EitherT.left`.
 */
case class EitherT[F[_], A, B](value: F[Either[A, B]]) {
  ....
}

Here’s UserRepo.followers with a dummy implementation:

scala> :paste
// Entering paste mode (ctrl-D to finish)
import cats._, cats.data._, cats.implicits._
object UserRepo {
  def followers(userId: Long)
    (implicit ec: ExecutionContext): EitherT[Future, Error, List[User]] =
    userId match {
      case 0L =>
        EitherT.right(Future { List(User(1, "Michael")) })
      case 1L =>
        EitherT.right(Future { List(User(0, "Vito")) })
      case x =>
        println("not found")
        EitherT.left(Future.successful { Error.UserNotFound(x) })
    }
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

import cats._
import cats.data._
import cats.implicits._
defined object UserRepo
import UserRepo.followers

Now let’s try writing isFriends0 function again.

scala> def isFriends3(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): EitherT[Future, Error, Boolean] =
         for{
           a <- followers(user1)
           b <- followers(user2)
         } yield a.exists(_.id == user2) && b.exists(_.id == user1)
isFriends3: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)cats.data.EitherT[scala.concurrent.Future,Error,Boolean]

Isn’t this great? Except for the type signature and the ExecutionContext, isFriends3 is identical to isFriends0.

Now let’s try using this.

scala> implicit val ec = scala.concurrent.ExecutionContext.global
ec: scala.concurrent.ExecutionContextExecutor = scala.concurrent.impl.ExecutionContextImpl@321e273f

scala> import scala.concurrent.Await
import scala.concurrent.Await

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> Await.result(isFriends3(0, 1).value, 1 second)
res0: Either[Error,Boolean] = Right(true)

When the first user is not found, EitherT will short circuit.

scala> Await.result(isFriends3(2, 3).value, 1 second)
not found
res34: cats.data.Xor[Error,Boolean] = Left(UserNotFound(2))

Note that "not found" is printed only once.

Unlike the StateTReaderTOption example, EitherT seems usable in many situations.

That’s it for today!