5

我有以下代码:

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 任务与主要算法分开,即与这个理解分开?或者我应该这样做吗?

4

2 回答 2

0

如果您的目标是避免将东西从一个 monad 转移到另一个,那么您可以使您的方法和接口具有多态性,以便它们可以与不同的 monad 一起工作,而不仅仅是 IO。以下是如何为您的IOMonad特质做到这一点:

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

这个想法是不承诺任何特定的 monad,而是让事情适用于任何提供特定用例所需功能的 monad。在IOMonad示例中,我们需要运行同步副作用的能力,因此我们通过传递 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())
  }
}

程序中的其他操作需要不同的功能。例如readDirection需要进行控制台 IO 并引发类型错误Throwable。引发错误的能力由MonadErrortrait 表示,所以你得到这个签名:

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

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

另一个例子是nextBlockcalculate。这些需要操作 type 的状态,GameState操作状态的能力由MonadStatetype 表示:

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-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, no State, no Either,连同这些,任何在不同 monad 之间转换或提升的必要性也消失了。

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

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

这就是为什么部分 Scala 社区(尤其是 John de Goes 和他的 ZIO 工作)不再鼓励 MTL 风格的原因。其他人一直在推动它,因为它允许代码与不同的效果类型一起重用。

于 2020-05-10T12:30:21.133 回答
0

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

    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什么?用户输入?

于 2019-09-18T10:23:37.867 回答