簡體   English   中英

如何使用 IO monad 編寫貓的理解

[英]How to write for comprehension in cats with IO monad

我有以下代碼:

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 任務與主算法分開,即從這個 for-comprehension 中分離出來? 還是我應該這樣做?

一個問題是您的Gameplay類型與IOGameplay不兼容,因為Gameplay使用Eval monad。 我假設你想要這個:

    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效果的預期目的是什么? 用戶輸入?

如果您的目標是避免將東西從一個 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是, MonadState不包含在貓或貓效應中,為此您需要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 ,沒有State ,沒有Either ,並且與這些一起,在不同的 monad 之間轉換或提升的任何必要性也消失了。

但是請注意,這種編程風格(稱為 MTL 風格)有其缺點。

  • 類型推斷通常不起作用。 在此示例中,您需要將F參數顯式傳遞給nextBlockcalculate ,因為 Scala 無法推斷它
  • 如前所述,cats 不包括所有必要的類型類,如MonadState ,所以你需要額外的庫,如cats-mtl
  • 對於新人來說有點難以理解

這就是為什么部分 Scala 社區(特別是 John de Goes 和他的 ZIO 努力)不再鼓勵 MTL 風格的原因。 其他人繼續推動它,因為它允許代碼以不同的效果類型重用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM