简体   繁体   中英

How to write for comprehension in cats with IO monad

I have the following code:

import cats.effect.IO
import cats.data.State
import cats.data.StateT
import cats.implicits._
import cats.effect.LiftIO

abstract class Example {
    object implicits {
        implicit def myEffectLiftIO: LiftIO[IOGameplay] =
            new LiftIO[IOGameplay] {
                override def liftIO[A](ioa: IO[A]): IOGameplay[A] = {
                    StateT.liftF(ioa)
                }
            }
    }

    type Gameplay[A] = State[GameState, A]
    type IOGameplay[A] = StateT[IO, GameState, A]
    type EitherDirection[A] = Either[Throwable, A]

    type Map = Array[Array[FieldType]]
    sealed trait FieldType
    case class GameState(map: Map, block: Block)
    case class Block(f1: Field, f2: Field) 
    case class Field()

    import implicits._
    val L = implicitly[LiftIO[IOGameplay]]

    sealed trait GameResult
    sealed trait Direction

    trait IOMonad {
        def println(msg: String): IO[Unit]
        def readln(): IO[String]
    }

    def play(io: IOMonad): StateT[IO, GameState, GameResult] = {
        val L = implicitly[LiftIO[IOGameplay]]

        for {
            // print map to the console
            _ <- L.liftIO(io.println("Next move: "))
            directionOpt <- L.liftIO(readDirection(io))
            direction <- StateT.liftF[IO, GameState, Direction](IO.fromEither(directionOpt))
            nextBlock <- IO(nextBlock(direction))
            gameResult <- calculate(nextBlock)
        } yield {
            gameResult
        }
    }

    def readDirection(io: IOMonad): IO[EitherDirection[Direction]]
    def nextBlock(direction: Direction): Gameplay[Block]
    def calculate(block: Block): Gameplay[GameResult]
}


This is not completely accurate, but I posted the whole block to explain the problem.
Here, I have many transformations on values to produce IO and to transform it to StateT. Is there a more clever way to do this? Maybe I should somehow separate io tasks from the main algorithm, ie from this for-comprehension? Or should I do it like this?

One issue is that your Gameplay type is not compatible with IOGameplay , since Gameplay uses the Eval monad. I assume you want this:

    type Gameplay[F[_], A] = StateT[F, GameState, A]
    type IOGameplay[A] = Gameplay[IO, A]

These methods need to return IOGameplay instances (or you could lift them in your program later):

    def nextBlock(direction: Direction): IOGameplay[Block]
    def calculate(block: Block): IOGameplay[GameResult]

Then the for-comprehension compiles with minor adjustments:

      for {
        // print map to the console
        _ <- L.liftIO(io.println("Next move: "))
        directionOpt <- L.liftIO(readDirection(io))
        direction <- StateT.liftF[IO, GameState, Direction](IO.fromEither(directionOpt))
        nextBlock <- nextBlock(direction)
        gameResult <- calculate(nextBlock)
      } yield {
        gameResult
      }

BTW, what is the intended purpose of the IO effect in this program? User input?

If your goal is to avoid lifting stuff from one monad to the other, then you can make your methods and interfaces polymorphic so that they can work with different monads and not just IO. Here's how to do that for your IOMonad trait:

  trait IOMonad[F[_]] {
    def println(msg: String): F[Unit]
    def readln(): F[String]
  }

The idea is to not commit to any specific monad, but to make things work for any monad that provides the features that you need for a specific use case. In the IOMonad example, we need the ability to run synchronous side effects, so we express that by passing a parameter of type Sync[F] :

import cats.effect.Sync
object IOMonad {
  def apply[F[_]](implicit F: Sync[F]) = new IOMonad[F] {
    def println(msg: String): F[Unit] = F.delay(println(msg))
    def readln(): F[String] = F.delay(scala.io.StdIn.readLine())
  }
}

The other operations in your program need different capabilities. For instance readDirection needs to do console IO and raise errors of type Throwable . The ability to raise errors is expressed by the MonadError trait, so you get this signature:

def readDirection[F[_]](
  io: IOMonad[F])(implicit monErr: MonadError[F, Throwable]
): F[Direction]

It's important to note that we're not passing a Sync[F] here, because we don't need it; the IOMonad[F] object is enough. This is important because it allows you to implement the IOMonad interface in some other way that doesn't necessarily involve side effects, notably for testing.

Another example are nextBlock and calculate . These need manipulate a state of type GameState , and the ability to manipulate state is expressed by the MonadState type:

def nextBlock[F[_]](
  direction: Direction)(implicit F: MonadState[F, GameState]
): F[Block]

def calculate[F[_]](
  block: Block)(implicit F: MonadState[F, GameState]
): F[GameResult]

MonadState is unfortunately not contained in cats or cats-effect, you need the cats-mtl library for that.

When you put all this together, you end up with a program like this:

import cats.MonadError
import cats.mtl.MonadState
import cats.implicits._

abstract class Example {
  type Map = Array[Array[FieldType]]
  sealed trait FieldType
  case class GameState(map: Map, block: Block)
  case class Block(f1: Field, f2: Field)
  case class Field()

  sealed trait GameResult
  sealed trait Direction

  trait IOMonad[F[_]] {
    def println(msg: String): F[Unit]
    def readln(): F[String]
  }

  def play[F[_]](
    io: IOMonad[F])(
    implicit merr: MonadError[F, Throwable],
    mst: MonadState[F, GameState]
  ): F[GameResult] = {
    for {
      // print map to the console
      _ <- io.println("Next move: ")
      direction <- readDirection(io)
      nextBlock <- nextBlock[F](direction)
      gameResult <- calculate[F](nextBlock)
    } yield gameResult
  }

  def readDirection[F[_]](
    io: IOMonad[F])(
    implicit merr: MonadError[F, Throwable]
  ): F[Direction]

  def nextBlock[F[_]](
    direction: Direction)(
    implicit merr: MonadState[F, GameState]
  ): F[Block]

  def calculate[F[_]](
    block: Block)(
    implicit mst: MonadState[F, GameState]
  ): F[GameResult]
}

Note that every single concrete Monad is gone – there is no IO , no State , no Either in the above program, and together with these, any necessity to convert or lift between different monads also went away.

Note however that this style of programming (known as MTL-Style) has its drawbacks.

  • type inference often doesn't work. In this example you need to pass the F parameter explicitly to nextBlock and calculate , because Scala can't infer it
  • as mentioned before, cats doesn't include all the necessary type classes like MonadState , so you need additional libraries like cats-mtl
  • it's somewhat hard to understand for newcomers

This is why parts of the Scala community (notably John de Goes and his ZIO effort) are no longer encouraging MTL-style. Others keep pushing it, because it allows code to be reused with different effect types.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM