·  阅读 1193

ZIO 是最近 Scala 社区非常热门且与众不同的 IO Monad 实现，本专题我们会从各个角度分析 ZIO 和 Cats-Effect 等 IO Monad 的设计。

``````trait Monad[F[_]] {
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
def pure[A](x: A): F[A]
}

``````object IO {
private case class Pure[A](value: A) extends IO[A]
private case class FlatMap[A, B](fa: IO[A], f: A => IO[B]) extends IO[B]
private case class Effect[A](run: () => A) extends IO[A]

def effect[A](f: () => A): IO[A] = Effect(f)

def pure[A](x: A) = IO.Pure(x)
def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = FlatMap(fa, f)
}
}

sealed trait IO[A] {
def unsafeRun(): A = this match {
case IO.Pure(a) => a
case IO.FlatMap(fa, f) => f(fa.unsafeRun).unsafeRun
case IO.Effect(run) => run()
}
}

`IO` 除了 `unsafeRun` 以外的所有操作都是纯函数，按照 FP 的潜规则，所有的副作用都只在应用边界触发，程序的可推理程度会大幅提升。

``````import cats.syntax.all._

object Console {
def getStr(): IO[String] = IO.effect(() => scala.io.StdIn.readLine())
def putStrLn(str: String): IO[Unit] = IO.effect(() => println(str))
}

def sum(i: IO[Int], j: IO[Int]): IO[Int] = {
i.flatMap { ii =>
j.map(jj => ii + jj)
}
}

Console.putStrLn(r.toString)
}
app.unsafeRun()

## 错误处理

``````val readInt = Console.getStr().map(_.toInt)

scala 程序通常会使用 `Try` 或者 `Either` 来表达一个结果可能存在失败，上述程序我们可以用 `Either``readInt` 改成以下形式：

`````` val readInt: IO[Either[String, Int]] = Console.getStr().map { str =>
try{
Right(str.toInt)
} catch {
case e: Exception => Left(s"\$str is not a number")
}
}

``````case class Failure[A](ex: Throwable) extends IO[A]
...
def unsafeRun(): A = this match {
case IO.Pure(a) => a
case IO.FlatMap(fa, f) => f(fa.unsafeRun).unsafeRun
case IO.Effect(run) => run()
case IO.Failure(ex) => throw ex
}

def pure[A](x: A) = IO.Pure(x)
def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = IO.FlatMap(fa, f)
def raiseError[A](e: Throwable) = IO.Failure(e)
def handleErrorWith[A](fa: IO[A])(f: Throwable => IO[A]): IO[A] = {
fa match {
case Failure(ex) => f(ex)
case io => io
}
}
}

``````case class NotAInt(str: String) extends Throwable
val readInt: IO[Int] = Console.getStr().flatMap { str =>
val r: Either[Throwable, Int] = try {
Right(str.toInt)
} catch {
case e: Throwable => Left(NotAInt(str))
}
}

``````val app = sum(readInt, readInt).flatMap { r =>
Console.putStrLn(r.toString)
}.recoverWith {
case NotAInt(str) => Console.putStrLn(s"\$str 不是一个字符串")
}

• 首先，所有的错误都必须是 `Throwable` 的子类型。
• 其次，所有的错误类型在程序中都泛化成了 `Throwable`，无法直观的从类型中看出错误类型，这样可能会导致调用者不知道需要处理什么错误。