在使用 State 和 IO 的堆叠 monad 时停止理解中流

Stop for-comprehension mid-flow when using stacked monads of State and IO

在这个 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()
  }

如果你想保持状态,那么你可以将另一个 monad 转换器加入其中,即 OptionT,用于 short-circuiting。整个区块包含可能 return 和 StopNow 的所有步骤然后被提升到 OptionT,最后使用 getOrElse:[=28 带回 BusinessIOState =]

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 returns a StopNow.


您可能想知道为什么不对 short-circuiting 使用 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 结果的情况下,状态没有正确传递,但是相反,所有内容都被删除,() 在 "end of the world" 处被 return 编辑。这有点类似于对控制流使用异常,但还有一个缺点,即它所能做的就是完全退出整个程序。所以,我可能会尝试使用 OptionT.