I am making 2 calls that both return the following:
def processUser(..): Future[Either[Error, UserStatus]]
def processTransactions(..): Future[Either[Error, TransactionStatus]]
So I could do it like this:
for {
_ <- processUser(user)
_ <- processTransaction(...)
}
The 2 calls don't depend on each other, but processUser
has to run before processTransaction
.
Here are my rules:
- if processUser returns an error, don't call
processTransaction
- if both succeed without an error, all is good
- if processUser returns successfully, but
processTransaction
fails with an error that I want to callunprocessUser
.
How can I do this? I guess using a for comprehension is not well suited for this, what pattern is more ideal?
Note
I am not using cats library, just plain Scala 2.13.x with Futures.
CodePudding user response:
If you use Cats
you can use something called a monad transformer, specifically EitherT
for your case:
import cats.implicits._
import cats.data.EitherT
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
type User
type UserStatus
type Transaction
type TransactionStatus
def processUser(user: User): Future[Either[Error, UserStatus]] = ???
def processTransaction(tx: Transaction): Future[Either[Error, TransactionStatus]] = ???
def unprocessUser(user: User): Future[Either[Error, TransactionStatus]] = ???
val user : User = ???
val tx : Transaction = ???
def resCatsFuture(): Future[Either[Error, Unit]] =
(for {
userStatus <- EitherT(processUser(user))
txStatus <- EitherT(processTransaction(tx))
.handleErrorWith(e => EitherT(unprocessUser(user)))
} yield ()).value
And since we are on Cats you should also consider replacing Future
with IO
from Cats Effect:
import cats.effect._
def processUserIO(user: User): IO[Either[Error, UserStatus]] = ???
def processTransactionIO(tx: Transaction): IO[Either[Error, TransactionStatus]] = ???
def unprocessUserIO(user: User): IO[Either[Error, TransactionStatus]] = ???
val resCatsIO: IO[Either[Error, Unit]] =
(for {
userStatus <- EitherT(processUserIO(user))
txStatus <- EitherT(processTransactionIO(tx))
.handleErrorWith(e => EitherT(unprocessUserIO(user)))
} yield ()).value
Or with ZIO
's typed errors as an alternative to monad transformers:
import zio._
def processUserZIO(user: User): ZIO[Any, Error, UserStatus] = ???
def processTransactionZIO(tx: Transaction): ZIO[Any, Error, TransactionStatus] = ???
def unprocessUserZIO(user: User): ZIO[Any, Error, TransactionStatus] = ???
val resZIO: ZIO[Any, Error, Unit] =
for {
userStatus <- processUserZIO(user)
txStatus <- processTransactionZIO(tx)
.catchAll(e => unprocessUserZIO(user))
} yield ()
If you want to stick to vanilla Scala you can use fold
on the inner Either
and return a new Future
for both Left
and Right
cases so you can keep your for-comprehension going.
def unprocessUser2(user: User): Future[Transaction] = ???
def unprocessUser3(user: User): Future[Unit] = ???
val res: Future[Unit] =
for {
mUserStatus <- processUser(user)
userStatus <- mUserStatus.fold(Future.failed, Future.successful) //you could also do `Future.fromTry(mUserStatus.toTry)` here
mTxStatus <- processTransaction(tx)
txStatus <- mTxStatus.fold(e => unprocessUser2(user), Future.successful)
//or alternatively, depending on how you want to handle the `Left` and Right values
_ <- mTxStatus.fold(e => unprocessUser3(user), tx => Future(println(tx)))
} yield ()
I'm making a lot assumptions about how unprocessUser
should work and where it should be handled. There are also other error handling methods in Cats and ZIO that might be better for your use case.
CodePudding user response:
for what you want, I think you don't need a for comprehension. Try this (please note that is writed dirrectly as an answer, can have some errors at compile)
processUser(user) map {
_ => processTransaction(user) flatMap {
_ => // do nothing
} recover {
case e => unprocessUser()
}
} recover {
case e => {
// throw err
}
}