简体   繁体   English

如何使用 IO monad 编写猫的理解

[英]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.在这里,我对值进行了许多转换以生成 IO 并将其转换为 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?也许我应该以某种方式将 io 任务与主算法分开,即从这个 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.一个问题是您的Gameplay类型与IOGameplay不兼容,因为Gameplay使用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):这些方法需要返回IOGameplay实例(或者你可以稍后在你的程序中提升它们):

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

Then the for-comprehension compiles with minor adjustments:然后 for-comprehension 编译时稍作调整:

      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?顺便说一句,这个程序中IO效果的预期目的是什么? 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.如果您的目标是避免将东西从一个 monad 提升到另一个,那么您可以使您的方法和接口多态,以便它们可以与不同的 monad 一起工作,而不仅仅是 IO。 Here's how to do that for your IOMonad trait:以下是为您的IOMonad特征执行此操作的方法:

  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.我们的想法是不承诺任何特定的 monad,而是让任何提供特定用例所需功能的 monad 工作。 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] :IOMonad示例中,我们需要能够运行同步副作用,因此我们通过传递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 .例如readDirection需要执行控制台 IO 并引发Throwable类型的错误。 The ability to raise errors is expressed by the MonadError trait, so you get this signature:引发错误的能力由MonadError特征表示,因此您将获得以下签名:

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;需要注意的是,我们没有在这里传递Sync[F] ,因为我们不需要它; the IOMonad[F] object is enough. IOMonad[F]对象就足够了。 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.这很重要,因为它允许您以不一定涉及副作用的其他方式实现IOMonad接口,特别是用于测试。

Another example are nextBlock and calculate .另一个例子是nextBlockcalculate These need manipulate a state of type GameState , and the ability to manipulate state is expressed by the MonadState type:这些需要操纵GameState类型的状态,操纵状态的能力由MonadState类型表示:

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. MonadState是, MonadState不包含在猫或猫效应中,为此您需要MonadState cats-mtl库。

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.请注意,每一个具体的 Monad 都没有了——在上面的程序中没有IO ,没有State ,没有Either ,并且与这些一起,在不同的 monad 之间转换或提升的任何必要性也消失了。

Note however that this style of programming (known as MTL-Style) has its drawbacks.但是请注意,这种编程风格(称为 MTL 风格)有其缺点。

  • 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在此示例中,您需要将F参数显式传递给nextBlockcalculate ,因为 Scala 无法推断它
  • as mentioned before, cats doesn't include all the necessary type classes like MonadState , so you need additional libraries like cats-mtl如前所述,cats 不包括所有必要的类型类,如MonadState ,所以你需要额外的库,如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.这就是为什么部分 Scala 社区(特别是 John de Goes 和他的 ZIO 努力)不再鼓励 MTL 风格的原因。 Others keep pushing it, because it allows code to be reused with different effect types.其他人继续推动它,因为它允许代码以不同的效果类型重用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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