Если вы хотите сохранить состояние, вы можете добавить еще один монадный преобразователь в микшер, а именно OptionT
, для короткого замыкания. Весь блок со всеми шагами, которые могут вернуть StopNow
, затем поднимается до OptionT
и, наконец, возвращается к BusinessIOState
, используя 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
}
Вывод:
dS
3
Обратите внимание, что 4
не появляется, программа останавливается раньше, потому что step3
возвращает StopNow
.
Вас может удивить, почему бы не использовать возможности MonadError[IO, Throwable]
для короткого замыкания, поскольку IO
уже может иметь дело с вычислениями, которые прекращаются из-за вызванного исключения. Вот как это может выглядеть:
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
}
Опять же, вывод:
dS
3
Я думаю, что он не короче и не чище по сравнению с версией OptionT
, а также имеет тот недостаток, что в случае результата StopNow
состояние не передается должным образом, а вместо этого все стирается, и ()
возвращается на «конце света». Это в некоторой степени аналогично использованию исключений для потока управления с дополнительным недостатком, заключающимся в том, что он может полностью выйти из всей программы. Итак, я бы, наверное, попробовал с OptionT
.