This Week in Cats: EitherT

Exceptions are the antithesis of functional programming since they break referential transparency, the code after resolving the exception cannot be substituted for the code prior to the exception. To avoid exceptions but still communicate errors to callers, we end up using the Either type throughly through the code. We also want our code to be async and so we also use the Future type in the result which leads to many methods like:

case class Session(sessionId: String, userId: String)
case class User(userId: String, firstName: String, lastName: String)

def fetchSessionFromCache(
    sessionId: String
): Future[Either[Throwable, Session]] = 
    Future.successful(Right(Session(sessionId, "123")))

def fetchUserFromDb(
   userId: String
): Future[Either[Throwable, User]] = 
    Future.successful(Right(User(userId, "Some", "User")))

Trying to chain the methods with for starts to become fairly painful. It would be nice if we could chain the method calls together without thinking about the Future. More generically, we have two methods of type F[Either[A, B]] and we want to work with them as if they were only of type Either[A,B]. This is where the EitherT, aka either transform, type comes into play.

def greetCurrentUser(
    sessionId: String
)(implicit 
    ec: ExecutionContext
): Future[Either[Throwable, String]] = {
    val result: EitherT[Future, Throwable, String] = for {
            session <- EitherT(fetchSessionFromCache(sessionId))
            user <- EitherT(fetchUserFromDb(session.userId))
        } yield s"Hello ${user.firstName}"
    result.value
}

Using the constructor/apply, we can lift values from their nested type into an EitherT and then manipulate them as if the outer type, Future in our example, doesn't exist. When we're done with our operations, we can restore the nested types with the value property.