Остановка для понимания среднего потока при использовании сложенных монад State и IO - PullRequest
3 голосов
/ 16 июня 2019

В этом примере Scala мне нужно остановиться, когда результат StopNow, мне нужно сделать это после вызова decisionStep.

Как я могу это сделать?

  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()
  }

1 Ответ

4 голосов
/ 17 июня 2019

Если вы хотите сохранить состояние, вы можете добавить еще один монадный преобразователь в микшер, а именно 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.

...