Cats IO

Cats IO

Cats IO 是一种将 side effect 编码为纯值的数据类型,它能够描述同步和异步计算。 介绍 IO[A] 类型的值代表一种计算模型,在对它进行求值时,它将执行某种效果运算并返回 A 类型的值。 IO 的值是纯的,不变的,因此保留了引用透明性,因此被用于函数式编程。 IO 是一种数据结构,仅表示对副作用计算的描述。 IO 可以描述以下同步或异步计算: 只能得到唯一解。 可能以成功结束或以失败结束,并且在失败的情况下,flatMap 链会被短路(IO 实现了 MonadError 代数结构)。 可以取消,但请注意,此功能依赖用户提供取消逻辑。 由抽象的过程所描述的 effect 将不会被执行直到“世界的尽头”,具体地说,直到某个“unsafe”方法被调用为止。 效果的执行结果是不被记忆的,这意味着内存开销最小(并且没有泄漏),并且,单个 effect 可以被以“引用透明”的方式被多次执行。例如: 上面的这个的例子,“hey!”被打印了两次,因为这个 effect 在“单子”链中被重复地执行。 引用透明和惰性求值 IO 可以“冻结” side effect,因为它是一种惰性求值的数据类型。请参考以下分类并反复与标准库中的“Future”的进行比较,来理解求值模型(在 Scala 语境中)的全貌。   Eager Lazy Synchronous A () => A     Eval[A] Asynchronous (A => Unit) => Unit () => (A => Unit) => Unit   Future[A] IO[A] 通过与 Scala 的 Future 比较,IO 数据类型即使在处理副作用时也保留了引用透明性,并且它是惰性求值的。与像 Scala 这样的即时求值语言相比较,这是结果与产生结果的函数之间的区别。 与 Future 类似,通过 IO,您可以推断异步处理的结果,但是由于其纯性和惰性,可以将 IO 视为一个规范(直到“世界的尽头”才进行求值),从而可以对 IO 的求值模型施加更多的控制,并且更具可预测性。例如当组合多个 IO,或处理错误时,是以序列化的方式处理,还是以并行的方式处理。 注意惰性求值总是与引用透明性并存。参考以下示例: 如果我们考虑引用透明性,则可以将该示例重写为: 但是这不适用于 Future,但适用于 IO,此能力对于函数式编程至关重要。 堆栈安全 IO 在它的 flatMap 中是以 trampoline 的方式进行求值的,这意味着您可以在任意深度的递归函数中安全地调用 flatMap,而不必担心会顶爆堆栈: 根据 IO 中实现的类型类的层次结构。除某些函数外,所有这里定义的操作都可用于 IO。 Effects 介绍 IO 是一种强大的抽象,它可以效果化地描述多种不同的 effect: 纯值 — IO.pure & IO.unit 在 IO 的伴随类中定义了以下函数,可以用于将纯值加载到 IO 中,从而生成出“已经求值”的 IO 值: 请注意,给定的参数形式是值传递而不是按名(by-name)传递。 例如,我们可以将一个数字(纯值)放入 IO 中,然后将其安全地与另一个打包了side effect 的 IO 组合在一起,因为它们将不执行任何操作: 显然,IO.pure 无法挂起副作用,因为当参数被以值传递给它时,IO.pure 是即时求值的,因此请不要这样做: 在这种情况下,println 将触发副作用,该副作用不会在 IO 中被挂起,有鉴于此,这样的代码可能不是我们想要的。 IO.unit 是 IO.pure(()) 的简化的别名,可在需要 IO[Unit] 值时重复使用,而无需担心触发任何其他副作用: IO[Unit] 在 Scala 代码中使用的相当普遍,Unit 类型本身就意味着调用的返回有副作用,它可以作为 pure(()) 的快捷方式,并且可以起到优化的用处,因为返回了相同的引用。 同步效果 — IO.apply 它可能是最常用的构建器,等效于 Sync[IO].delay,描述了可以在当前线程和调用堆栈上立即做求值的 IO 操作: 请注意,给定的参数是通过“by-name”传递的,其执行被“暂停”在 IO 上下文中。 这个示例是在 JVM 之上使用阻塞 I/O 从控制台读取和写入信息: 然后,我们可以以纯函数的方式使用它们来对与控制台的交互进行建模: 异步 […]

运行 Effects

ZIO 提供了多种不同的方式在您的应用程序中运行您的 effect。 App 如果您将整个程序构建一个 effect,那么运行 effect 的最自然的方法是扩展zio.App。 此类提供了 Scala 的 main 函数,因此可以从IDE调用它,或从命令行启动它。您所需要做的就是实现 run 方法,命令行参数被保存在 List 中作为参数传递给它: 如果您自定义了应用程序的环境,则必须(使用 ZIO#provide来)为您的 effect 的 run 函数提供环境,因为您(自定义的)的 App不知道如何将自定义的环境提供给 effect。 缺省的运行时 大多数应用程序不是全新的,必须与遗留代码,过程库和框架集成。 在这些情况下,更好解决方案是创建一个运行时,将其传递给需要运行 effect 的地方。 ZIO 包含一个名为 Runtime.default 的默认运行时。此运行时将所有 ZIO 提供的模块的实现(包括 Console,System,Clock,Random,Scheduler以及在 JVM 上的 Blocking)捆绑在一起,并且可以运行需要这些模块任意组合的 effect。 要使用这个运行时,只需使用: 一旦您获得了一个运行时,你就可以将它用于执行您的 effect: 除了unsafeRun方法之外,还有其他方法可以异步执行 effect 或将其转换为Future。 定制 Runtime 如果您使用自定义环境运行程序,那么创建专门针对该环境量身定制的运行时可能会很有用。 以下两个值用来创建自定义的 custom Runtime[R]: R Environment. 这是执行 effect 时将提供的环境。This is the environment that will be provided to effects when they are executed. Platform. 这是 ZIO 启动运行时系统所需的平台。 例如,以下代码使用 ZIO 提供的默认平台创建了一个可以将 Int 作为环境值提供给effect 的运行时: 错误报告 作为运行时的一部分,每一个 platform 都包含一个错误报告程序,ZIO 将调用该错误报告程序以报告每个未经处理的错误。提供您自己的错误报告程序是个好主意,比如可以将未处理的错误记录到文件中。 默认的未处理错误报告器仅将错误输出到标准错误输出。 Next Steps 如果您对运行效果感到满意,那就恭喜! 现在,您可以学习 ZIO 网站的其他部分,包括数据类型,用例以及与其他系统的互操作。. 有关所有核心 ZIO 类型和方法的详细文档,请参考 Scaladoc。

Effects测试

有许多方法可以测试 effect,包括使用 free monad,使用 tagless-final 和使用环境 effect。尽管所有这些方法都与 ZIO 兼容,但最简单,最符合习惯的是 环境 effect。 本节介绍 环境 effect,并向您展示如何使用它们来编写可测试的功能代码。 环境 ZIO 数据类型有一个类型参数 R,它用来描述 effect 所需的环境类型。 ZIO effects 可以使用 ZIO.environment 来访问环境,通过它直接得到 R 类型的环境值: 环境不必是整数等原始类型。它可能要复杂得多,比如可以是一个 trait 或 case class。 如果环境带有属性字段,则可以通过 ZIO.access 单个调用直接访问环境的给定部分: 甚至 effect 本身也可以存储在环境中!在这种情况下,要访问和执行 effect,可以使用 ZIO.accessM 方法。 如上例所示,从环境访问 effect 时,该效果称为 environment effect。 稍后,我们将看到环境 effect 是怎样提供一种简便的方法来测试 ZIO 应用程序的。 提供 Environments 必须先为 effect 提供(providing)环境,然后它们才能运行。 最简单的为一个 effect 提供所需环境的方法是使用 ZIO#provide 函数: 您为 effect 提供了所需的环境后,如果其返回的 effect 的环境类型为 Any (UIO[_]),这表明其要求已完全得到满足。 ZIO.accessM 和 ZIO#provide 的组合对于充分地利用环境 effect 来使得测试变得简单是必需的。 环境化的 Effects 环境 effect 背后的基本思想是面向接口编程,而非面向实现。对于函数式语言 Scala 而言,接口不包含任何具有副作用的方法,但是它们可能包含返回函数化了的effect 的方法。 与其在整个代码库中手动地实现依赖注入或者使用不连贯的隐式来传递接口,不如使用 ZIO Environment 来进行繁重的工作,从而使代码优雅,可推断且轻松自如。 在本节中,我们将通过开发可测试的数据库服务来探索如何使用环境 effect。 定义服务(Service) 我们以模块的形式定义数据库服务,该模块是仅包含单个字段的接口,该字段提供对服务的访问。 在这个例子中,Database 代表 模块,它包含了 Database.Service 服务。 这个 服务 只是一个普通的接口,定义在模块的伴随对象中,包含了该服务提供的功能函数。 辅助函数 为了简化访问数据库服务的环境 effect,我们将定义一些辅助函数来调用 ZIO.accessM。 虽然这些辅助函数不是必须的,因为我们可以直接通过 ZIO.accessM 来访问数据库模块,但是这些帮助程序易于编写并且使得调用端的代码看上去更简单。 调取服务 我们已经定义了一个模块和辅助函数,现在我们准备构建一个使用数据库服务的示例: 在此示例中,effect 仅能通过环境与数据库进行交互,在这种情况下,环境就成为一个提供对数据库服务的访问的模块。 要实际运行这个 effect,我们仅需要提供数据库模块的实现。 实现服务实体 现在我们将实现数据库模块的服务实体,它将切实负责与我们的生产厂数据库的交互: 在以上这段代码片段中,我们不打算提供这两个数据库方法的实现,因为这将需要超出本教程的范围。 运行数据库 Effect 现在我们有了一个数据库模块,和于数据库模块交互的辅助方法,可以一个数据库模块的具体实现。 我们现在使用  ZIO.provide 将数据库模块的实现提交给我们的应用程序: 自此,我们对产生出的 effect 没有了进一步的要求,所以现在可以将它提交给运行时去执行了。 实现服务的测试案例 为了测试代码于数据库的交互,我们不需要提供一个真实的数据库,因为这样的话我们的测试将会很慢很脆弱,并且即便应用的逻辑都是正确的也可能产生随机性的错误。 虽然我们可以使用模拟(mocking)库来创建测试模块,但是在本节中,我们将直接直接创建一个测试模块,以表明这里面没有任何魔术: 由于此模块仅在测试中使用,因此我们通过硬编码读取和更新 map 中的数据来模拟于数据库的交互。为了测试模块具有纤程安全性,可以使用 Ref 而不是 var 来定义 map。 测试数据库代码 现在要测试请求数据库的代码,我们只需要为它提供这个用于测试的数据库模块就可以了: 我们的应用程序代码可以像测试数据库模块一样使用在生产数据库模块上。 Next Steps 如果您对测试效果感到满意,那么下一步就是学习运行 effects。

BACK TO TOP