[英]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
.另一个例子是nextBlock
和calculate
。 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 风格)有其缺点。
F
parameter explicitly to nextBlock
and calculate
, because Scala can't infer it在此示例中,您需要将F
参数显式传递给nextBlock
并calculate
,因为 Scala 无法推断它MonadState
, so you need additional libraries like cats-mtl
如前所述,cats 不包括所有必要的类型类,如MonadState
,所以你需要额外的库,如cats-mtl
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.