In this Scala example I need to stop when the result is StopNow
, I need to do this after calling decisionStep
.
How can I do that?
case class BusinessState()
trait BusinessResult
case object KeepGoing extends BusinessResult
case object StopNow extends BusinessResult
type IOState[S, A] = StateT[IO, S, A]
type BusinessIOState[A] = IOState[BusinessState, A]
trait SomeSteps {
def step1:BusinessIOState[Unit]
def step2:BusinessIOState[BusinessState]
def decisionStep:BusinessIOState[BusinessResult]
def step3:BusinessIOState[BusinessResult]
def step4:BusinessIOState[BusinessResult]
def program = for {
_ <- step1
businessState <- step2
businessResult <- decisionStep
businessResult1 <- step3
businessResult2 <- step4
} yield()
}
If you want to keep the state, then you could throw yet another monad transformer into the mix, namely OptionT
, for short-circuiting. The entire block with all the steps that might return a StopNow
is then lifted into OptionT
, and finally brought back to BusinessIOState
using getOrElse
:
import cats._
import cats.data._
import cats.syntax._
import cats.effect._
object StopIO extends IOApp {
case class BusinessState()
trait BusinessResult
case object KeepGoing extends BusinessResult
case object StopNow extends BusinessResult
type IOState[S, A] = StateT[IO, S, A]
type BusinessIOState[A] = IOState[BusinessState, A]
trait SomeSteps {
def step1: BusinessIOState[Unit]
def step2: BusinessIOState[BusinessState]
def decisionStep: BusinessIOState[BusinessResult]
def step3: BusinessIOState[BusinessResult]
def step4: BusinessIOState[BusinessResult]
def toOpt(a: BusinessIOState[BusinessResult])
: OptionT[BusinessIOState, BusinessResult] = {
OptionT.liftF(a).filter(_ == KeepGoing)
}
def program: BusinessIOState[Unit] = (for {
_ <- step1
businessState <- step2
_ <- (for {
_ <- toOpt(decisionStep)
_ <- toOpt(step3)
_ <- toOpt(step4)
} yield ()).getOrElse(())
} yield ())
}
object Impl extends SomeSteps {
def step1 = Monad[BusinessIOState].unit
def step2 = Monad[BusinessIOState].pure(BusinessState())
def decisionStep = StateT.liftF(IO { println("dS"); KeepGoing })
def step3 = StateT.liftF(IO { println("3"); StopNow })
def step4 = StateT.liftF(IO { println("4"); KeepGoing })
}
def run(args: List[String]) = for {
_ <- Impl.program.runA(BusinessState())
} yield ExitCode.Success
}
The output is:
dS
3
Note that the 4
does not appear, the program stops earlier, because step3
returns a StopNow
.
You might wonder why not use the capabilities of MonadError[IO, Throwable]
for short-circuiting, since IO
already can deal with computations that stop because of a thrown exception. Here is what it might look like:
import cats._
import cats.data._
import cats.syntax._
import cats.effect._
object StopIO extends IOApp {
case class BusinessState()
trait BusinessResult
case object KeepGoing extends BusinessResult
case object StopNow extends BusinessResult
type IOState[S, A] = StateT[IO, S, A]
type BusinessIOState[A] = IOState[BusinessState, A]
trait SomeSteps {
def step1: BusinessIOState[Unit]
def step2: BusinessIOState[BusinessState]
def decisionStep: BusinessIOState[BusinessResult]
def step3: BusinessIOState[BusinessResult]
def step4: BusinessIOState[BusinessResult]
def raiseStop(a: BusinessIOState[BusinessResult])
: BusinessIOState[Unit] = {
a.flatMap {
case KeepGoing => StateT.liftF(IO.unit)
case StopNow => StateT.liftF(
MonadError[IO, Throwable].raiseError(new Exception("stop now"))
)
}
}
def program = (for {
_ <- step1
businessState <- step2
_ <- raiseStop(decisionStep)
_ <- raiseStop(step3)
_ <- raiseStop(step4)
} yield ())
}
object Impl extends SomeSteps {
def step1 = Monad[BusinessIOState].unit
def step2 = Monad[BusinessIOState].pure(BusinessState())
def decisionStep = StateT.liftF(IO { println("dS"); KeepGoing })
def step3 = StateT.liftF(IO { println("3"); StopNow })
def step4 = StateT.liftF(IO { println("4"); KeepGoing })
}
def run(args: List[String]) = for {
_ <- Impl.program.runA(BusinessState()).handleErrorWith(_ => IO.unit)
} yield ExitCode.Success
}
Again, the output is:
dS
3
I think that it's neither shorter nor clearer compared to the OptionT
version, and it also has the disadvantage that in case of a StopNow
result, the state is not passed on properly, but instead everything is erased, and a ()
is returned at the "end of the world". This is somewhat analogous to using exceptions for control flow, with an additional disadvantage that all it can do is to exit the whole program altogether. So, I'd probably try it with OptionT
.