繁体   English   中英

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

[英]How to write for comprehension in cats with IO monad

我有以下代码:

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]
}


这并不完全准确,但我发布了整个块来解释问题。
在这里,我对值进行了许多转换以生成 IO 并将其转换为 StateT。 有没有更聪明的方法来做到这一点? 也许我应该以某种方式将 io 任务与主算法分开,即从这个 for-comprehension 中分离出来? 还是我应该这样做?

一个问题是您的Gameplay类型与IOGameplay不兼容,因为Gameplay使用Eval monad。 我假设你想要这个:

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

这些方法需要返回IOGameplay实例(或者你可以稍后在你的程序中提升它们):

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

然后 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
      }

顺便说一句,这个程序中IO效果的预期目的是什么? 用户输入?

如果您的目标是避免将东西从一个 monad 提升到另一个,那么您可以使您的方法和接口多态,以便它们可以与不同的 monad 一起工作,而不仅仅是 IO。 以下是为您的IOMonad特征执行此操作的方法:

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

我们的想法是不承诺任何特定的 monad,而是让任何提供特定用例所需功能的 monad 工作。 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())
  }
}

程序中的其他操作需要不同的功能。 例如readDirection需要执行控制台 IO 并引发Throwable类型的错误。 引发错误的能力由MonadError特征表示,因此您将获得以下签名:

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

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

另一个例子是nextBlockcalculate 这些需要操纵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是, MonadState不包含在猫或猫效应中,为此您需要MonadState cats-mtl库。

当你把所有这些放在一起时,你最终会得到一个这样的程序:

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]
}

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

但是请注意,这种编程风格(称为 MTL 风格)有其缺点。

  • 类型推断通常不起作用。 在此示例中,您需要将F参数显式传递给nextBlockcalculate ,因为 Scala 无法推断它
  • 如前所述,cats 不包括所有必要的类型类,如MonadState ,所以你需要额外的库,如cats-mtl
  • 对于新人来说有点难以理解

这就是为什么部分 Scala 社区(特别是 John de Goes 和他的 ZIO 努力)不再鼓励 MTL 风格的原因。 其他人继续推动它,因为它允许代码以不同的效果类型重用。

暂无
暂无

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

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