2.7 更多的Effect构造函数
在本章的前面,我们了解了如何使用ZIO.effect构造函数将过程代码转换为ZIO效果。ZIO.effect构造函数是一种有用且通用的效果构造函数,但并不适合所有情况:
- 易犯错误。 ZIO.effect构造函数返回的效果可能因任何Throwable类型(
ZIO [Any,Throwable,A]
)而失败。 当您将遗留代码转换为ZIO并且不知道它是否会引发异常时,这是正确的选择,但是有时候,我们知道某些代码不会引发异常(例如检索系统时间)。 - 同步。 ZIO.effect构造函数要求我们的过程代码是同步的,并从捕获的代码块中返回指定类型的某些值。 但是在异步API中,我们必须注册一个回调,以便在类型A的值可用时调用。 我们如何将异步代码转换为ZIO效果?
- 展开。 ZIO.effect构造函数假定我们正在计算的值没有包装在另一个数据类型中,该数据类型具有自己的建模失败方式。 但是,我们与之交互的一些代码返回了
Option [A]
,Either [E,A]
,Try [A]
甚至是Future [A]
。 我们如何从这些类型转换为ZIO效果?
幸运的是,ZIO有许多强大的构造函数,可自定义失败处理方案,异步代码和其他常见数据类型。
2.7.1 纯值与不纯值
在介绍其他ZIO effect构造函数之前,我们首先需要讨论参照透明性。如果我们始终可以在任何程序中将计算替换为其结果,同时仍保留其运行时行为,则诸如2 + 2之类的表达式将是透明的。
例如,考虑以下表达式:
val sum: Int = 2 + 2
我们可以将表达式2 + 2替换为其结果4,我们的程序的运行结果不会有任何改变。
相反,请考虑以下简单程序,该程序从控制台读取一行输入,然后将其输出到控制台:
import scala.io.StdIn
val echo: Unit = {
val line = StdIn.readLine()
println(line)
}
我们无法将 echo 的代码块替换为其结果并维持程序的行为。 echo的结果值只是Unit值,因此,如果将echo替换为其返回值,则将具有:
val echo: Unit = ()
这两个程序肯定不一样。 第一个从用户读取输入,并将输入打印到控制台,但是第二个程序什么也不做!
我们无法用其计算结果代替echo
的原因是它会产生副作用。 它在一边做事(从控制台读取和写入控制台)。 这与参照透明函数相反,后者没有副作用,仅计算值。没有副作用的表达式称为纯表达式,而主体为纯表达式的函数称为纯函数。ZIO.effect构造函数采用副作用代码,并将其转换为纯值,该值仅描述副作用。
为了了解这一点,让我们重新了解回声程序的ZIO实现:
import zio._
val readLine = ZIO.effect(StdIn.readLine())
def printLine(line: String) = ZIO.effect(println(line))
val echo = for {
line <- readLine
_ <- printLine(line)
} yield ()
该程序是参照透明的,因为它仅建立了工作计划,而没有产生任何副作用。我们可以用生成的蓝图替换构建此蓝图的代码,而我们仍然对此echo
程序有一个计划。
因此,我们可以将参照透明性作为另一种角度,去理解"函数式作用是工作蓝图"。函数式作用通过描述副作用而不是执行它们,从而使副作用代码具有参照透明性。
描述和执行之间的这种分离,解耦"怎么做"和"做什么"之间的混乱,并赋予我们巨大的力量来变换和合成effect,正如我们在本书中将会看到的那样。
参照透明性是将代码转换为ZIO时的一个重要概念,因为如果值或函数是参照透明的,则我们无需将其转换为ZIO effect。但是,如果不纯净,则需要使用正确的effect构造函数将其转换为ZIO效果。
即使您不小心将副作用代码视为纯代码,ZIO也会尝试做正确的事情。但是,将副作用代码与ZIO代码混合在一起可能会导致bug的产生,因此,最好谨慎使用正确的effect构造函数。作为附带的好处,这将使您的代码更易于同事阅读和查看。
2.7.2用于纯计算的effect构造器
These constructors are useful primarily when combining other ZIO effects, which have been constructed from side-effecting code, with pure code.
ZIO带有各种effect构造器,可将纯值转换为ZIO effect。 这些构造函数在和从其他副作用代码和纯代码构造的ZIO效果组合在一起时非常有用。
此外,即使是纯代码也可以从ZIO的某些功能中受益,例如环境,键入错误和堆栈安全性。
将纯值转换为ZIO效果的两种最基本的方法是成功和失败:
object ZIO {
def fail[E](e: => E): ZIO[Any, E, Nothing] = ???
def succeed[A](a: => A): ZIO[Any, Nothing, A] = ???
}
- ZIO.succeed构造函数将一个值转换为以该值成功的effect。 例如,
ZIO.succeed(42
)构造一个以值42成功的effect。ZIO.succeed返回的效果的失败类型为Nothing,因为使用此构造函数创建的效果不会失败。 - ZIO.fail构造函数将值转换为因该值而失败的效果。 例如,
ZIO.fail(new Exception)
构造一个因指定的异常而失败的效果。 ZIO.fail返回的效果的成功类型为Nothing,因为使用此构造函数创建的效果无法成功。
我们将看到无法成功的效果,无论是因为它们失败还是因为它们永远运行,通常都将Nothing用作成功类型。
除了这两个基本构造函数外,还有许多其他构造函数可以将标准Scala数据类型转换为ZIO效果。
import scala.util.Try
object ZIO {
def fromEither[E, A](eea: => Either[E, A]): IO[E, A] = ???
def fromOption[A](oa: => Option[A]): IO[None.type, A] = ???
def fromTry[A](a: => Try[A]): Task[A] = ???
}
这些构造函数将原始数据类型的成功和失败案例转换为ZIO成功和错误类型。
ZIO.fromEither构造函数将Either [E,A]
转换为IO [E,A]
效果。如果Either是Left,则生成的ZIO效果将以E失败,但是如果是Right,则生成的ZIO效果将以A成功。
ZIO.fromTry构造函数类似,不同之处在于错误类型固定为Throwable,因为Try只能通过Throwable失败。
ZIO.fromOption构造函数更有趣,它说明了一个经常出现的想法。请注意,错误类型为None.type。这是因为Option只有一种错误模式。 Option [A]
是带有值的Some [A]
,或者是None,没有其他信息。
因此,一个Option可能会失败,但实际上只有一种可能会失败的方法-值为None。该唯一失败值的类型为None.type。
这些不是纯值唯一的效果构造函数。在本章结尾的练习中,您将探索其他一些构造函数。
2.7.3用于副作用计算的 effect 构造器
最重要的效果构造函数是用于副作用计算的那些。 这些构造函数将过程代码转换为ZIO effect,因此它们成为将内容与方法分开的蓝图。
在本章的前面,我们介绍了ZIO.effect。 该构造函数捕获有副作用的代码,并将其计算推迟到以后,将代码中引发的所有异常转换为ZIO.fail值。
但是,有时我们想将副作用代码转换为ZIO效果,但是我们知道副作用代码不会引发任何异常。 例如,检查系统时间或生成随机变量绝对是副作用,但是它们不能引发异常。
对于这些情况,我们可以使用构造函数ZIO.effectTotal,它将程序代码转换为不会失败的ZIO效果:
object ZIO {
def effectTotal[A](a: => A): ZIO[Any, Nothing, A]
}
2.7.3.1 异步回调转换
JVM生态系统中的许多代码都是非阻塞的。 非阻塞代码不会同步计算并返回值。 相反,当您调用异步函数时,必须提供一个回调。然后,当该值可用时,将使用该值调用您的回调。 (有时这被隐藏在Future或其他异步数据类型的后面。)
例如,假设我们在以下代码段中显示了非阻塞查询API:
def getUserByIdAsync(id: Int)(cb: Option[String] => Unit): Unit = ???
如果我们给此函数一个我们感兴趣的id,它将在后台查找用户,但立即返回。 然后,在检索到用户之后,它将调用我们传递给该方法的回调函数。
在类型签名中使用Option表示没有用户使用我们请求的ID。
在以下代码段中,我们调用getUserByIdAsync,并传递一个回调,该回调将在接收到用户名时简单地打印出该用户名:
getUserByIdAsync(0) {
case Some(name) => println(name)
case None => println("User not found!")
}
请注意,对getUserByIdAsync的调用几乎会立即返回,即使在调用回调函数之前需要一段时间(甚至几秒钟或几分钟),并且实际上会将用户名打印到控制台上。
基于回调的API可以提高性能,因为我们可以编写更高效的代码而不会浪费线程。 但是直接使用基于回调的异步代码可能会很痛苦,导致代码高度嵌套,从而难以将成功和错误信息传播到正确的位置,并且无法安全地处理资源。
幸运的是,就像之前的Scala的Future一样,ZIO允许我们获取异步代码,并将其转换为ZIO功能效果。
我们需要执行此转换的构造函数是ZIO.effectAsync,其类型签名显示在以下片段中:
object ZIO {
def effectAsync[R, E, A](cb: (ZIO[R, E, A] => Unit) => Any): ZIO[R, E, A] =
???
}
ZIO.effectAsync的类型签名可能很难理解,因此让我们来看一个示例。
要将getUserByIdAsync过程代码转换为ZIO,我们可以使用ZIO.effectAsync构造函数,如下所示:
def getUserById(id: Int): ZIO[Any, None.type, String] = ZIO.effectAsync {
callback =>
getUserByIdAsync(id) {
case Some(name) => callback(ZIO.succeed(name))
case None => callback(ZIO.fail(None))
}
}
effectAsync提供的回调,并期望返回ZIO效果,因此,如果用户存在于数据库中,则我们使用ZIO.succeed将用户名转换为ZIO效果,然后使用此成功效果调用回调。另一方面,如果用户不存在,则使用ZIO.fail将None转换为ZIO效果,然后使用此失败的效果调用回调。
我们不得不花点时间将异步代码转换为ZIO函数,但是现在使用此查询API时,我们不再需要处理回调。现在,我们可以像对待任何其他ZIO函数一样对待getUserById,并使用flatMap之类的方法来组合其返回值,而这些函数都不会阻塞,并且具有ZIO为我们提供围绕资源安全性的所有保证。
一旦getUserById计算的结果可用,我们将继续创建的蓝图中的其他计算。
请注意,在effectAsync中,回调函数只能被调用一次,因此不适用于转换所有异步API。如果回调可能被调用多次,则可以在ZStream上使用effectAsync构造函数,这将在本书后面讨论。
我们将在本章中介绍的最后一个构造函数是ZIO.fromFuture,它将将创建Future的函数转换为ZIO效果。
此构造函数的类型签名如下:
def fromFuture[A](make: ExecutionContext => Future[A]): Task[A] = ???
因为Future是正在运行的计算,所以我们在执行此操作时必须非常小心。 fromFuture构造函数不需要Future。 相反,构造函数采用函数ExecutionContext => Future [A]
,该函数描述了如何在给定ExecutionContext的情况下制作Future。
尽管在将Future转换为ZIO效果时不需要使用提供的ExecutionContext,但是如果您使用上下文,则ZIO可以管理Future在更高级别上的运行位置。
如果可能,我们要确保我们的make函数实现创建一个新的Future,而不是返回已经运行的Future。 以下代码显示了执行此操作的示例:
def goShoppingFuture(implicit
ec: ExecutionContext
): Future[Unit] =
Future(println("Going to the grocery store"))
val goShopping: Task[Unit] = Task.fromFuture(implicit ec => goShoppingFuture)
还有许多其他构造函数可以从其他数据类型(例如java.util.concurrent.Future)创建ZIO效果,并提供第三方包以提供从Monix,Cats Effect和其他数据类型的转换。