如何使用IO monad在猫中撰写理解力

时间:2019-06-30 23:36:12

标签: scala scala-cats cats-effect

我有以下代码:

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任务与主要算法(即对此理解)分开?还是应该这样?

2 个答案:

答案 0 :(得分:0)

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

    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 {
        // 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效果的预期目的是什么?用户输入了吗?

答案 1 :(得分:0)

如果您的目标是避免将东西从一个monad提升到另一个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并不包含在cats或cats-effect中,您需要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都不用了-上述程序中没有IOStateEither,也没有这些,因此没有任何转换或解除的必要不同的单子之间也消失了。

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

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

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