ZIO和Cats Effect:成功的联盟

Cats Effect已成为功能性Scala世界的一种“反应流”,可让您将整个多样化的图书馆生态系统结合在一起。

许多出色的库:http4s,fs2,doobie-仅基于Cats Effect中的类型类实现。 ZIO和Monix之类的库又为它们的效果类型提供了这些类型类的实例。 尽管在3.0版中将解决一些问题,但Cats Effect帮助许多开源贡献者有机地支持Scala语言的整个功能生态系统。 使用Cats Effect的开发人员面临一个艰难的选择:将哪种效果实现用于其应用程序。

今天有三种选择:

  • Cat IO,参考实现;
  • Monix,任务数据类型及其在代码中的反应性;
  • ZIO,ZIO数据类型及其跨线程范围。

在本文中,我将尝试向您证明,使用Cats Effect创建您的应用程序,ZIO是与Cats IO中的参考实现完全不同的设计解决方案和功能的理想选择。

1.更好的MTL /无标签最终架构


MTL(Monad变形金刚库)是一种编程风格,其中函数根据其作用类型而多态,并通过“类型类约束”表达其要求。 在Scala中,这通常称为“无标签最终样式”(尽管不是同一回事),尤其是在类型类没有法律的情况下。

众所周知,无法为经典的MTL类型类(如Writer和State)以及效果类型(如Cats IO)定义全局实例。 问题在于,针对这些类型的效果的这些类型类的实例需要访问可变状态,该状态不能全局创建,因为创建可变状态也是一种效果。

但是,为了获得最佳性能,重要的是要避免使用“ monad变换器”,并在主要效果类型之上直接提供Write和State实现。

为此,Scala程序员使用了一个技巧:他们在程序的顶层创建(但干净的)具有效果的实例,然后将它们作为局部含义在程序中进一步提供:

Ref.make[AppState](initialAppState).flatMap(ref => implicit val monadState = new MonadState[Task, AppState] { def get: Task[AppState] = ref.get def set(s: AppState): Task[Unit] = ref.set(s).unit } myProgram ) 

尽管这种技巧很有用,但它仍然是“拐杖”。 在理想情况下,类型类的所有实例可以是连贯的(每种类型一个实例),而不是在本地创建,从而产生效果,然后将其神奇地包装在隐式值中,以供后续方法使用。

MTL /无标签最终处理的一个伟大功能是,您可以使用ZIO环境在ZIO数据类型之上直接定义大多数实例。

这是为ZIO数据类型创建全局MonadState定义的一种方法:

 trait State[S] { def state: Ref[S] } implicit def ZIOMonadState[S, R <: State[S], E]: MonadState[ZIO[R, E, ?], S] = new MonadState[ZIO[R, E, ?], S] { def get: ZIO[R, E, S] = ZIO.accessM(_.state.get) def set(s: S): ZIO[R, E, Unit] = ZIO.accessM(_.state.set(s).unit) } 

现在,为至少支持State[S]任何环境全局定义一个实例。

FunctorListen类似,也称为MonadWriter

 trait Writer[W] { def writer: Ref[W] } implicit def ZIOFunctorListen[W: Semigroup, R <: Writer[W], E]: FunctorListen[ZIO[R, E, ?], W] = new FunctorListen[ZIO[R, E, ?], W] { def listen[A](fa: ZIO[R, E, A]): ZIO[R, E, (A, W)] = ZIO.accessM(_.state.get.flatMap(w => fa.map(a => a -> w))) def tell(w: W): ZIO[R, E, W] = ZIO.accessM(_.state.update(_ |+| w).unit) } 

当然,我们可以对MonadError执行相同的MonadError

 implicit def ZIOMonadError[R, E]: MonadError[ZIO[R, E, ?], E] = new MonadError[ZIO[R, E, ?], E]{ def handleErrorWith[A](fa: ZIO[R, E, A])(f: E => ZIO[R, E, A]): ZIO[R, E, A] = fa catchAll f def raiseError[A](e: E): ZIO[R, E, A] = ZIO.fail(e) } 

此技术很容易应用于其他类型类,包括无标签最终类型类,其实例可能需要生成效果(更改,配置),测试生成效果的函数(将环境效果与无标签最终组合)或从环境中容易访问的任何其他方法。

不再需要缓慢的单调转换! 假设在初始化类的实例时产生效果,以本地含义为“否”。 不再需要拐杖。 直接沉浸在纯函数式编程中。

2.为凡人节省资源


ZIO的最初功能之一是中断– ZIO运行时能够立即中断任何可执行效果并保证释放所有资源的能力。 Cats IO对该功能的粗略实现。

Haskell称这种功能为异步异常,它使您可以创建并有效使用延迟,有效的并行和竞争操作以及全局最佳计算。 这种中断不仅带来巨大的好处,而且在支持对资源的安全访问方面也带来了复杂的任务。

程序员用于通过简单分析来跟踪程序中的错误。 ZIO也可以做到这一点,ZIO使用类型系统来帮助检测错误。 但是中断是另外一回事。 由许多其他效果创建的效果可以在任何边界处中断。

考虑以下效果:

 for { handle <- openFile(file) data <- readFile(handle) _ <- closeFile(handle) } yield data 

大多数开发人员在这种情况下不会感到惊讶:如果readFile崩溃, closeFile将不会执行。 幸运的是,效果系统具有ensuring (Cats Effect中的guarantee ),使您可以将最终处理程序添加到终结器效果中,类似于final。

因此,上面代码的主要问题可以轻松解决:

 for { handle <- openFile(file) data <- readFile(handle).ensuring(closeFile(handle)) } yield () 

从某种意义上说,现在的效果已经变得“抗摔”,如果readFile ,文件仍将关闭。 如果readFile成功,该文件也将关闭。 在所有情况下,文件都会关闭。

但仍然一点也不。 中断意味着效果可以在任何地方中断,即使在openFilereadFile之间也可以中断。 如果发生这种情况,打开的文件将不会关闭,并且会发生资源泄漏。

获取和释放资源的模式是如此广泛,以至于ZIO引入了括号运算符,该运算符也出现在Cats Effect 1.0中。 Bracket语句可防止中断:如果成功接收资源,则即使中断使用该资源的效果,也会发生释放。 此外,资源的接收和释放都不会中断,从而提供了资源安全性的保证。

使用方括号,上面的示例如下所示:

 openFile(file).bracket(closeFile(_))(readFile(_)) 

不幸的是,方括号仅封装了一种(相当普遍的)资源消耗模式。 还有许多其他功能,尤其是具有竞争性的数据结构,中断必须访问这些数据结构,否则可能导致泄漏。

通常,所有中断工作都归结为两点:

  • 防止在某些可能被中断的区域中出现中断;
  • 允许在可能结冰的区域打断。

ZIO可以同时实现两者。 例如,我们可以使用低级ZIO抽象来开发我们自己的支架版本:

 ZIO.uninterruptible { for { a <- acquire exit <- ZIO.interruptible(use(a)) .run.flatMap(exit => release(a, exit) .const(exit)) b <- ZIO.done(exit) } yield b } 

在此代码中, use(a)是唯一可以中断的部分。 周围的代码保证在任何情况下都可以执行release

您可以随时检查是否有中断的机会。 为此,仅需要两个原始操作(其余所有操作均从这些操作派生)。

这种组合式功能齐全的中断模型使您不仅可以实现简单的括号实现,而且还可以实现资源管理中的其他方案,在这些方案中,可以在中断的优缺点之间找到平衡。

Cat IO仅提供一种用于控制中断的操作:不可取消的组合器。 它使整个代码块不间断。 尽管很少使用此操作,但它可能导致资源泄漏或锁定。

同时,事实证明您可以在Cats IO中定义一个原语,从而使您可以更好地控制中断。 Fabio Labella的非常复杂的实现过程极其缓慢。

ZIO允许您编写带有中断的代码,使用声明性复合运算符进行高级操作,而不会强迫您在严重的复杂性,低性能和阻止泄漏之间进行选择。

此外,最近在ZIO中添加的软件事务性存储器使用户可以声明性地编写自动异步,竞争性并允许中断的数据结构和代码。

3.有保证的终结者


许多编程语言中的try / finally块提供了编写同步代码而不泄漏资源所必需的保证。

特别是,此块可保证以下各项:如果try块开始执行,则try停止时将执行finally块。

此保修适用于:

  • 有嵌套的“ try / finally”块;
  • “ try块”中有错误;
  • 嵌套的finally块中有错误。

ZIO“确保”操作可以像try / final一样使用:

 val effect2 = effect.ensuring(cleanup) 

ZIO为“ effect.ensuring(finalizer)”提供以下保证:如果开始执行“ effect”,则当“ effect”停止时,“ finalizer”将开始执行。

像try / final,这些保证在以下情况下仍然存在:

  • 有嵌套的“确保”成分;
  • “效果”中存在错误;
  • 嵌套的“ finalizer”中有错误。

而且,即使影响被打断,保证仍然得以维持(“括号”的保证是相似的,实际上,“保证”是实施“括号”)。

Cats IO数据类型提供了另一个较弱的保证。 对于“ effect.guarantee(finalizer)”,它的弱点如下:如果开始执行“ effect”,则当问题效果未插入“ effect”时,“ finalizer”将在“ effect”停止时开始执行。

在Cats IO中执行“括号”时,也发现较弱的保证。

要获取资源泄漏,只需使用“ guarantee”或“ bracket.use”效果中使用的效果,并使用以下内容进行组合:

 //   `interruptedFiber` -    val bigTrouble = interruptedFiber.join 

当以这种方式将bigTrouble插入到另一个效果中时,该效果将不会中断-将不会执行通过“保证”设置的“终结器”或通过“括号”进行的资源清理。 即使块中存在“ finalizer”,所有这些都会导致资源消耗。

例如,以下代码中的“ finalizer”将永远不会开始执行:

 (IO.unit >> bigTrouble).guarantee(IO(println("Won't be executed!!!«))) 

在不考虑全局上下文的情况下评估代码时,无法确定是否将诸如“ bigTrouble”之类的效果插入“ bracket”操作的“ use”效果中的任何位置或“ finalizer”块内。

因此,如果不评估整个程序,就无法找出Cats IO程序是否可以解决资源泄漏或缺少“ finalizer”块的问题。 整个程序只能手动评估,并且此过程总是伴随着编译器无法验证的错误。 此外,每次代码中发生任何重要更改时,都必须重复此过程。

ZIO具有Cats Effect,“ guaranteeCase”和“ bracket”中的“保证”的自定义实现。 实现使用本地ZIO语义(而不是Cats IO语义),这使我们能够在此时和现在评估资源泄漏的可能问题,并且知道在所有情况下都将启动终结器并释放资源。

4.稳定的切换


Cats Effect具有“ ContextShift”中的“ evalOn”方法,该方法使您可以将某些代码的执行转移到另一个执行上下文。

出于多种原因,这很有用:

  • 许多客户端库迫使您在其线程池中执行一些工作;
  • UI库要求在UI线程中进行一些更新。
  • 有些影响需要隔离以适应其特定功能的线程池。

“ EvalOn”操作在应运行的位置执行效果,然后返回到原始执行上下文。 例如:

 cs.evalOn(kafkaContext)(kafkaEffect) 

注意:Cats IO具有类似的“ shift”构造,该构造使您无需回退即可切换到其他上下文,但是在实践中,这种行为很少需要,因此首选“ evalOn”。

“ evalOn”的ZIO实现(在ZIO原语“锁”上进行)提供了必要的保证,可以唯一地了解效果在哪里工作-效果将始终在特定的上下文中执行。

Cats IO的保证则有所不同,但较弱-效果将在特定上下文中执行,直到第一次异步操作或内部切换为止。

考虑一小段代码,不可能确定是否会在要切换的效果中内置异步效果(或嵌套切换),因为异步不会显示在类型中。

因此,与资源安全性一样,要了解Cats IO效果将在何处启动,有必要研究整个程序。 在实践中,根据我的经验,当在一种情况下使用“ evalOn”时,Cats IO用户感到惊讶的是,随后发现大多数效果是在另一种情况下意外执行的。

ZIO允许您确定应在何处触发效果,并且不管效果如何内置到其他效果中,都可以相信它会在所有情况下发生。

5.错误消息的安全性


任何支持并发,并发或对资源的安全访问的影响都将陷入线性错误模型:通常,并非所有错误都可以保存。

对于“ Throwable”,Cats IO内置的固定错误类型以及ZIO支持的多态错误类型,都是如此。

具有多个一次性错误的情况的示例:

  • 终结器抛出异常;
  • 两个(下降)效果在并行执行中组合在一起;
  • 赛车状态下的两种(下降)影响;
  • 中断的效果会下降,然后才使该部分免受中断。

由于未保存所有错误,因此ZIO提供了基于自由半环(来自抽象代数的抽象,此处不提供其知识)的“原因[E]”数据结构,该结构允许针对任何类型的错误连接串行和并行错误。 在所有操作期间(包括清除掉落的或中断的效果),ZIO会将错误汇总到“原因[E]”数据结构中。 该数据结构随时可用。 结果,ZIO始终存储所有错误:它们始终可用,可以根据业务需求进行记录,研究和转换。

Cats IO选择了具有错误信息丢失的模型。 ZIO将通过原因[E]连接两个错误,而Cats IO将“丢失”错误消息之一,例如,通过对发生的错误调用“ e.printStackTrace()”。

例如,此代码中“ finalizer”中的错误将丢失。

 IO.raiseError(new Error("Error 1")).guarantee(IO.raiseError(new Error("Error 2«))) 

这种跟踪错误的方法意味着您无法本地定位和处理由于各种影响而发生的所有错误。 ZIO允许您使用任何类型的错误,包括“ Throwable”(或更具体的子类型,如“ IOExceptio”或其他自定义异常层次结构),以确保在程序执行期间不会丢失任何错误。

6.异步无死锁


ZIO和Cats IO都提供了一个构造函数,使您可以通过回调获取代码并将其包装

此功能是通过Cats Effect中的Async管道类提供的:

 val effect: Task[Data] = Async[Task].async(k => getDataWithCallbacks( onSuccess = v => k(Right(v)), onFailure = e => k(Left(e)) )) 

这将创建一个异步效果,该效果在执行时将等待,直到该值出现,然后再继续,所有这些对于效果用户而言都是显而易见的。 因此,函数式编程对于开发异步代码非常有吸引力。

请注意,一旦回调代码变成了效果,就会调用回调函数(在此称为“ k”)。 该回调函数以成功/错误值退出。 调用此回调函数后,将恢复执行效果(先前已暂停)。

如果未将效果分配给任何特定的特殊上下文或未附加该效果的另一个上下文,则ZIO保证该效果将在运行时线程池上恢复执行。

Cats IO恢复对回调线程的影响。 这些选项之间的差异非常深:导致回调的线程不希望回调代码永远执行,而只允许在控件返回之前稍有延迟。 另一方面,Cats IO根本不提供这样的保证:调用线程,启动回调可能冻结,等待执行控制返回时的不确定时间。

Cats Effect中的竞争数据结构的早期版本(“ Deferred”,“ Semaphore”)恢复了无法将执行控制权返回给调用线程的效果。 结果,在其中发现了与死锁和执行调度程序损坏有关的问题。 尽管已发现所有这些问题,但仅在Cats Effect中解决了竞争数据结构的问题。

使用与Cats IO中类似的方法的用户代码会遇到此类麻烦,因为此类任务是不确定的,因此在运行时错误很少会发生,这使得调试和问题检测变得困难。

ZIO提供了开箱即用的死锁保护和常规任务调度程序,还使用户可以明确选择Cats IO的行为(例如,在“ Promise”上使用“ unsafeRun”,最终会恢复异步效果)。

尽管这些解决方案都不适合所有情况,并且ZIO和Cats IO提供了足够的灵活性来解决所有情况(以不同的方式),但是选择ZIO意味着使用“异步”而无需担心,并迫使您将问题代码置于“ unsafeRun”中,已知会导致死锁

7.与未来兼容


对于大量代码库而言,使用Scala标准库中的“ Future”是现实。 ZIO带有“ fromFuture”方法,该方法提供了现成的执行上下文:

 ZIO.fromFuture(implicit ec => // Create some Future using `ec`: ??? ) 

当此方法用于包装效果中的Future时,ZIO可以设置执行Future的位置,而其他方法(例如evalOn)将Future正确地传输到所需的执行上下文。 Cats IO接受使用外部“ ExecutionContext”创建的“ Future”。 这意味着Cats IO无法根据evalOn或shift方法的要求移动Future的执行。 此外,这给用户增加了确定Future的执行上下文的负担,这意味着选择范围狭窄且环境独立。

由于可以忽略提供的ExecutionContext,因此ZIO可以表示为Cats IO功能的总和,在通常情况下可以确保与Future的交互更加顺畅和准确,但是仍然有例外。

8.阻塞IO


如文章“ 线程池。 使用ZIO的最佳做法 ,对于服务器应用程序,至少需要两个单独的池才能实现最大效率:

  • 用于CPU /异步效果的固定池;
  • 动态的,并可能增加阻塞线程的数量。

在固定线程池上运行所有影响的决定有一天会导致死锁,而在动态池上触发所有影响则可能导致性能损失。

在JVM上,ZIO提供了两种支持阻塞效果的操作:

  • “阻塞(效果)运算符,它在具有良好预设的阻塞线程池中切换某种效果的执行,可以根据需要更改这些预设”;
  • «effectBlocking(effect)» , , .

, , , «blocking». , - , , «effectBlocking» , ZIO ( ).

Cats IO , . , «blocking», «evalOn», , , .

( ZIO) (, ), .

9.


, Scala, :

  • «ReaderT»/ «Kleisli», ;
  • «EitherT», ( «OptionT», «EitherT» «Unit» ).

, (, http4s «Kleisli» «OptionT»). («effect totation»), ZIO «reader» «typed error» ZIO. «reader» «typed error» , ZIO , . , «Task[A]», «reader» «typed errors».

ZIO () - . , ZIO , .

Cats IO . , , «reader» «typed errors» «state», «writer» , .

ZIO 8 Cats IO . , Scala .

10.


ZIO , , . , Scala, .

ZIO 2000 , «typed errors» , — 375 . Scala , . , , .

:

  • ;
  • ;
  • , ;
  • .

. , - , .

- . , . ZIO . Cats IO , , ZIO ( , ).

11.


ZIO , , - .

  • , : «ZIO. succeed» «Applicative[F].pure», «zip» «Apply[F].product», «ZIO.foreach» «Traverse[F].traverse».
  • (Cats, Cats Effect, Scalaz ).
  • , ( «Runtime», Cats Effect - Cats Effect). — Cats IO.
  • .
  • . : "zip"/"zipPar", "ZIO.foreach"/"ZIO.foreachPar", "ZIO.succeed"/"ZIO.succeedLazy«.
  • , «». ZIO IDE.
  • Scala ZIO : «ZIO.fromFuture», «ZIO.fromOption», «ZIO.fromEither», «ZIO.fromTry».
  • «».

, Scala, , ZIO , , , ZIO, . Cats IO , Cats.

, , , ( , , ).

12.


ZIO — - , .

:

  • , «Ref», «Promise», «Queue», «Semaphore» «Stream» //;
  • STM, , , ;
  • «Schedule», ;
  • «Clock», «Random», «Console» «System» , ;
  • , .

- Cats IO . Cats IO , ( ) .

结论


Cats Effect Scala-, , .

, Cats Effect, , Cats Effect : Cats IO, Monix, Zio.

, . , , , : ZIO Cats Effect .

Scala — . , Scala. ScalaConf , 18 , John A De Goes .

Source: https://habr.com/ru/post/zh-CN472386/


All Articles