Transactional Effects in Scala
Composing effects with rollbacks in case of failure
Join the DZone community and get the full member experience.
Join For FreeIntroduction
This post was inspired by a StackOverflow answer for the question Compose Futures with Recovery in Scala: Is it possible to compose Future
s in a manner where, if one of them fails, a previous Future
's execution result will be rolled back via some function?
The Problem
The case with Future
can be generalized to some effect F[_]
, so we can say: How can we compose effects F[_]
in a manner where, if one of them fails, the previous F[_]
execution result will be rolled back via some function? This behavior is similar to what we know as a Transaction.
Solution
For the code solution below cats-effect
version 2.3.1
used. The final solution can look like this:
x
/**
* Transactional effect provide possibility to recover effect execution result if it failed, but still return failed
* result.
*
* @tparam F surrounding effect type
*/
class TransactionEffect[F[_], E](underlying: F[E], rollback: PartialFunction[Throwable, F[Unit]])
(implicit F: FlatMap[F], ME: MonadError[F, Throwable]) {
/*
* Here goes syntax trick - in for-comprehension will be invoked `flatMap` of this wrapper and not of underlying effect.
*/
def flatMap[S](f: E => F[S]): F[S] = {
F.flatMap(underlying)(f).recoverWith {
case exception: Throwable =>
val failure: F[S] = ME.raiseError[S](exception)
rollback.lift(exception).fold(failure)(recoverEffect => F.flatMap(recoverEffect)(_ => failure))
}
}
}
object TransactionEffect {
/**
* Provides syntax sugar over [[TransactionEffect]] wrapper.
*/
implicit class TransactionEffectSyntax[F[_], E](underling: F[E])
(implicit F: FlatMap[F], AE: MonadError[F, Throwable]){
def rollbackWith(rollback: PartialFunction[Throwable, F[Unit]]): TransactionEffect[F, E] = {
new TransactionEffect[F, E](underling, rollback)
}
}
}
And small demo:
xxxxxxxxxx
object TransactionEffectDemo extends IOApp {
import TransactionEffect._
override def run(args: List[String]): IO[ExitCode] = {
for {
_ <- IO.delay(println("A executed")).rollbackWith {
case _: Throwable => IO.delay(println("A recovered"))
}
_ <- IO.delay(println("B executed")).rollbackWith {
case _: Throwable => IO.delay(println("B recovered"))
}
_ <- IO.raiseError(new Exception("C failed"))
} yield ExitCode.Success
}
}
Which will print out the next output (made shorter for the sake of the example):
A executed
B executed
B recovered
A recovered
java.lang.Exception: C failed
Also, you can play with demo in Scatie: https://scastie.scala-lang.org/W8qmBrVQRx6izAVo3Q9yvg
Real-Life Usage
If an application is written in the pure Functional Programming paradigm, ideally it should not have any side effects that it needs to "roll-back" in case of another effect failure. But this approach can be useful when, for instance, the app stores documents in more than one database, e.g. Mongo and Elasticsearch, and removes the document from the main MongoDB if storing in Elasticsearch failed.
Thank you for your attention and hope it will be helpful!
Published at DZone with permission of Ivan Kurchenko. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments