3

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()
  }
jakstack
  • 2,143
  • 3
  • 20
  • 37

1 Answers1

4

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.

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • thanks, what would it look like if i need to print the final BusinessResult? – jakstack Jun 17 '19 at 05:37
  • @jakstack You mean, for example, if `StopNow` was actually a `StopNow(reason)`, you'd want to see the `reason`? Well, then take an `EitherT` instead of `OptionT`, and let it store the exact reason why it exited earlier, and then don't throw it away by overriding it with `()`, but print it instead. – Andrey Tyukin Jun 17 '19 at 10:30