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 college days writing ancient seal scripts and carving seals:
「大学では、はんこを刻ったり、篆書を書いてました」
「えっ?なぜプログラマに???」 pic.twitter.com/DEhqy4ELpF
— Kenji Yoshida (@xuwei_k) October 21, 2013
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:
Future[A]
comes up a lot in Scala.
Future
everywhere.
Future
is asynchronous, any errors need to be captured in there.
Future
is designed to handle Throwable
, but that’s all you get.
Future
of Either
?
Here’s the prepration step:
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
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.
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)
Now suppose we want to make the database access async
so we changed the followers
to return a Future
like this:
import scala.concurrent.{ Future, ExecutionContext }
object UserRepo {
def followers(userId: Long): Future[Either[Error, List[User]]] = ???
}
import UserRepo.followers
Now, how would isFriends0
look like? Here’s one way of writing this:
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)
And here’s another version:
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))
}
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 function that uses Future[Either[Error, A]]
.
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:
import cats._, cats.data._, cats.syntax.all._
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
Now let’s try writing isFriends0
function again.
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)
Isn’t this great? Except for the type signature and the ExecutionContext
,
isFriends3
is identical to isFriends0
.
Now let’s try using this.
{
implicit val ec = scala.concurrent.ExecutionContext.global
import scala.concurrent.Await
import scala.concurrent.duration._
Await.result(isFriends3(0, 1).value, 1 second)
}
// res2: Either[Error, Boolean] = Right(value = true)
When the first user is not found, EitherT
will short circuit.
{
implicit val ec = scala.concurrent.ExecutionContext.global
import scala.concurrent.Await
import scala.concurrent.duration._
Await.result(isFriends3(2, 3).value, 1 second)
}
// not found
// res3: Either[Error, Boolean] = Left(value = UserNotFound(userId = 2L))
Note that "not found"
is printed only once.
Unlike the StateTReaderTOption
example, EitherT
seems usable in many situations.
That’s it for today!