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. 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? 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. 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):
def nextBlock(direction: Direction): IOGameplay[Block]
def calculate(block: Block): IOGameplay[GameResult]
Then the for-comprehension compiles with minor adjustments:
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? 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. Here's how to do that for your IOMonad
trait:
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. 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]
:
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
. The ability to raise errors is expressed by the MonadError
trait, so you get this signature:
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; the IOMonad[F]
object is enough. 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.
Another example are nextBlock
and calculate
. These need manipulate a state of type GameState
, and the ability to manipulate state is expressed by the MonadState
type:
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.
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.
Note however that this style of programming (known as MTL-Style) has its drawbacks.
F
parameter explicitly to nextBlock
and calculate
, because Scala can't infer itMonadState
, so you need additional libraries like cats-mtl
This is why parts of the Scala community (notably John de Goes and his ZIO effort) are no longer encouraging MTL-style. Others keep pushing it, because it allows code to be reused with different effect types.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.