我有以下代码:
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任务与主要算法(即对此理解)分开?还是应该这样?
答案 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
接口,这种接口不一定会带来副作用,尤其是对于测试。
另一个示例是nextBlock
和calculate
。这些需要操纵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都不用了-上述程序中没有IO
,State
,Either
,也没有这些,因此没有任何转换或解除的必要不同的单子之间也消失了。
但是请注意,这种编程样式(称为MTL样式)有其缺点。
F
参数显式传递给nextBlock
和calculate
,因为Scala无法推断出MonadState
,因此您需要其他库,例如cats-mtl
这就是为什么Scala社区的某些成员(特别是John de Goes和他的ZIO努力)不再鼓励采用MTL风格的原因。其他人则继续推动它,因为它允许代码以不同的效果类型重复使用。