已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

Effect 与 fp-ts 对比

对比 Effect 与 fp-ts,涵盖类型化服务、资源管理、并发与 Stream 处理等特性。

关键进展

  • 项目合并:fp-ts 项目已正式并入 Effect-TS 生态。fp-ts 的作者 Giulio Canti 受邀加入了 Effect 组织。更多细节参见这则公告
  • 延续与演进:Effect 可以看作 fp-ts v2 的继任者,实际上等同于 fp-ts v3,标志着该库能力的一次重大演进。

常见问题

Effect 与 fp-ts 的打包体积对比

问:我用 Effect 和 fp-ts 分别写了两个简单的程序并比较它们的打包体积,为什么 Effect 的打包体积更大?

答:观察到打包体积不同是很自然的,因为 Effect 和 fp-ts 是两套为不同目的而设计的系统。 Effect 的打包体积更大,是因为它内置了 Fiber 运行时,而这对它的功能至关重要。 初始打包体积看起来可能偏大,但随着你不断使用 Effect,这部分开销会被逐渐摊薄。

问:在 Effect 和 fp-ts 之间做选择时,我需要担心打包体积的差异吗?

答:不一定。你应该结合项目的具体需求,以及这两个库各自能带来的收益来判断。

Effect 中的 Micro 模块被设计成标准 Effect 模块的轻量级替代品,专门面向对减小打包体积有严格要求的场景。 该模块是自包含的,不包含 LayerRefQueueDeferred 这类更复杂的功能。 一旦用到任何主要的 Effect 模块(OptionEitherArray 这类基础数据模块之外),effect 运行时就会被加入你的打包产物,Micro 带来的好处也随之消失。 因此,对于希望在尽量不影响打包体积的前提下实现 Effect 能力的库来说,Micro 是理想选择,尤其是那些计划对外暴露基于 Promise 的 API 的库。 它还支持这样的场景:客户端使用 Micro,而服务端使用 Effect 的完整功能集,从而在同一应用的不同部分之间保持兼容并共享逻辑。

对比表

下表对比了 Effect 与 fp-ts 两个库的特性。

特性fp-tsEffect
类型化服务
内置服务
类型化错误
可管道化的 API
Dual API
可测试性
资源管理
中断
Defect
基于 Fiber 的并发
Fiber 监督
重试与重试策略
内置日志
内置调度
内置缓存
内置批处理
指标
追踪
配置
不可变数据结构
Stream 处理

下面逐一解释每项特性:

类型化服务

fp-ts 和 Effect 都能在类型层面跟踪需求,让你可以定义并使用具有特定类型的服务。在 fp-ts 中,你可以使用 ReaderTaskEither<R, E, A> 类型;而在 Effect 中,则有 Effect<A, E, R> 类型可用。需要注意的是,在 fp-ts 中 R 类型参数是逆变的,这意味着无法保证避免冲突,而且该库只提供了用于依赖管理的基础工具。

另一方面,在 Effect 中 R 类型参数是协变的,当涉及多个 effect 时,所有 API 都能在类型层面合并依赖。Effect 还提供了一系列专门设计的工具来简化依赖管理,包括 TagContextLayer。这些工具让你处理代码中的依赖时更轻松、更灵活,也更容易组合和管理复杂应用。

内置服务

Effect 内置了 ClockRandomTracer 这类服务,而 fp-ts 不提供任何默认服务。

类型化错误

两个库都支持类型化错误,让你可以定义并处理具有特定类型的错误。不过,在 Effect 中,当涉及多个 effect 时,所有 API 都能在类型层面合并错误,而且每个 effect 都可能以不同类型的错误失败。

这意味着,组合多个可能失败的 effect 时,最终的错误类型会是各个错误类型的并集。Effect 提供了各种工具与类型层面的操作,来高效地处理和管理这些合并后的错误类型。

可管道化的 API

fp-ts 和 Effect 都提供可管道化的 API,让你能借助 pipe 函数以函数式且可读的方式组合和串联操作。不过 Effect 更进一步,为每种数据类型都提供了 .pipe() 方法,这样处理管道时更方便,不必每次都显式导入 pipe 函数。

Dual API

Effect 提供 dual API,让你可以用不同的方式使用同一个 API(例如 “data-last” 与 “data-first” 两种变体)。

可测试性

fp-ts 的函数式风格总体上有利于写出易于测试的代码,但该库本身并没有提供专门为测试阶段设计的工具。而 Effect 在可测试性上更进一步,提供了额外的、专门用于简化测试流程的工具。

Effect 提供了一系列提升可测试性的工具。例如,它提供了 TestClock,让你可以在测试中控制时间的流逝,这对测试依赖时间的代码很有用。此外,Effect 还提供了 TestRandom,让你能对涉及随机性的代码进行完全确定性的测试,从而保证测试结果一致且可预测。另一个有用的工具是 ConfigProvider.fromMap,它让你在测试期间可以轻松地为应用定义 mock 配置。

资源管理

Effect 内置了资源管理能力,而 fp-ts 在这方面的功能很有限(主要是 bracket),并且不够成熟完善。

在 Effect 中,资源管理指的是以安全且受控的方式获取和释放资源(例如数据库连接、文件句柄或网络套接字)的能力。该库提供了全面而精细的机制来处理资源的获取与释放,确保正确清理并防止资源泄漏。

中断

Effect 支持中断,也就是说,你可以在需要时中断并取消正在进行的计算。这一特性让你对代码的执行有更强的控制力,也能应对希望在计算完成前就停止它的场景。

在 Effect 中,当你需要处理用户取消、超时,或者其他要求停止正在进行的计算的外部事件时,中断就非常有用。你可以显式地请求中断,库会安全而高效地停止该计算的执行。

另一方面,fp-ts 没有对中断的内置支持。在 fp-ts 中,一个计算一旦开始,就会一直运行到完成或遇到错误,无法在中途被中断。

Defect

Effect 提供了处理 defect、管理非预期失败的机制。在 Effect 中,defect 指的是程序执行期间可能出现的非预期错误或失败。

借助 Effect,你可以用内置的工具与实用函数以结构化且可靠的方式处理 defect。它提供的错误处理能力让你能够捕获并处理异常、从失败中恢复,并优雅地应对非预期场景。

另一方面,fp-ts 没有专门用于管理 defect 的内置支持。虽然你可以用标准的函数式编程技术在 fp-ts 中处理错误,但 Effect 在处理 defect 方面提供了更全面、更精简的方式。

基于 Fiber 的并发

Effect 利用基于 Fiber 的并发,从而实现轻量而高效的并发计算。简单来说,基于 Fiber 的并发允许多个任务同时运行,让你的代码响应更快、效率更高。

借助基于 Fiber 的并发,Effect 能以轻量且不阻塞其他任务执行的方式处理并发操作。这意味着你可以同时运行多个计算,充分利用可用资源并最大化性能。

另一方面,fp-ts 没有对基于 Fiber 的并发的内置支持。虽然 fp-ts 提供了丰富的函数式编程特性,但它在并发计算方面的支持程度不及 Effect。

Fiber 监督

Effect 提供了用于管理和监控 Fiber 的监督策略。fp-ts 没有对 Fiber 监督的内置支持。

重试与重试策略

Effect 内置了按可自定义的重试策略重试计算的支持。fp-ts 开箱即用并不提供这一功能,你需要依赖外部库才能实现类似能力。不过要注意,外部库在精细程度和可调优程度上,可能比不上 Effect 内置的重试能力。

重试功能让你可以在计算或操作失败时,依据一组预定义的规则或策略自动重试。当你面对不可靠或不可预测的资源(例如网络请求或外部服务)时,这尤其有用。

Effect 提供了一整套全面的重试策略,你可以按自己的具体需求进行定制。这些策略定义了重试计算的条件,例如重试次数、重试之间的延迟,以及判断是否应当尝试重试的标准。

借助 Effect 内置的重试功能,你可以更稳健、更有弹性地处理瞬时错误或临时性失败。这有助于提升应用整体的可靠性与稳定性,特别是在需要与外部系统或服务交互的场景中。

相比之下,fp-ts 不提供重试计算的内置支持。如果你需要在 fp-ts 中使用重试功能,就得依赖外部库,而这些库可能无法提供与 Effect 相同水平的精细度与灵活性。

值得一提的是,Effect 内置的重试能力被设计为能与其错误处理、资源管理等其他特性无缝协作。这种集成让处理计算中的失败与重试有了更统一、更全面的方式。

内置日志

Effect 自带日志能力。这意味着你可以轻松地把日志集成到应用中,而不需要额外的库或依赖。此外,Effect 提供的默认 logger 可以替换为自定义 logger,以满足你特定的日志需求。

日志是软件开发中不可或缺的一环,它让你能在代码执行期间记录并跟踪重要信息,帮助你监控应用行为、调试问题,并收集可供分析的洞察。

借助 Effect 内置的日志能力,你可以轻松地在代码的各个位置记录消息、警告、错误或其他相关信息。这对于跟踪执行流程、发现潜在问题,或捕获应用运行期间的重要事件尤其有用。

另一方面,fp-ts 不提供内置的日志能力。如果你需要在 fp-ts 中使用日志功能,就得依赖外部库,或者从零实现自己的日志方案,这会给代码库带来额外的复杂度和依赖。

内置调度

Effect 提供内置的调度能力,让你可以按时间管理计算的执行。fp-ts 不具备这一特性。

在许多应用中,常常会有需要按特定间隔执行、或安排在未来执行的任务或计算。例如,你可能希望定期更新数据、触发通知,或在特定时间运行后台进程。内置调度在这些场景下就派上了用场。

另一方面,fp-ts 没有内置的调度能力。如果你需要在 fp-ts 中安排任务或管理定时计算,就必须依赖外部库,或者自己实现调度机制,这会增加代码库的复杂度。

内置缓存

Effect 提供内置的缓存机制,让你可以缓存计算结果以提升性能。fp-ts 不具备这一特性。

在许多应用中,计算可能很耗时或耗费资源,尤其是在处理复杂操作或访问远程资源时。缓存是一种存储计算结果的技术,这样就能快速取回结果,而不必每次都重新计算。

借助 Effect 内置的缓存能力,你可以轻松缓存计算结果并在需要时复用。通过避免重复计算、减轻对外部资源的压力,这能显著提升应用的性能。

内置批处理

Effect 提供内置的批处理能力,让你可以把多个计算合并为一次批量计算。fp-ts 不具备这一特性。

在许多场景中,你可能需要执行多个共享相似输入或依赖的计算。逐个执行这些计算会造成效率低下和额外开销。批处理是一种把这些计算分组、以单个批次执行的技术,能提升性能并减少不必要的处理。

指标

Effect 内置了对收集和上报指标的支持,这些指标与计算及系统行为相关。它特别支持 OpenTelemetry 指标。fp-ts 不具备这一特性。

指标在理解和监控应用的性能与行为方面起着关键作用。它们能就响应时间、资源利用率、错误率等诸多方面提供有价值的洞察。通过收集和分析指标,你可以定位性能瓶颈、优化代码,并做出有依据的决策来提升应用的整体质量。

追踪

Effect 内置了追踪能力,让你可以追踪和调试代码的执行,并跟踪一个请求在应用中的流转路径。此外,Effect 还提供了专门的 OpenTelemetry exporter,用于与 OpenTelemetry 可观测性框架集成。相比之下,fp-ts 没有提供类似的追踪工具来增强代码执行的可见性。

配置

Effect 内置了对在计算中管理和访问配置值的支持。fp-ts 不具备这一特性。

配置值是软件开发中不可或缺的一部分。它们让你无需修改代码就能定制应用的行为。配置值的例子包括数据库连接字符串、API 端点、功能开关,以及各种可能随环境或部署而变化的设置。

借助 Effect 内置的配置支持,你可以轻松地在计算中管理和访问这些值。它提供了便捷的工具和抽象来加载、校验和访问配置值,确保应用拥有正常运行所需的设置。

利用 Effect 内置的配置支持,你可以:

  • 从各种来源加载配置值,例如环境变量、配置文件或远程配置提供方。
  • 校验并确保加载的配置值符合预期的格式与结构。
  • 在计算中访问配置值,从而在需要的任何地方使用它们。

不可变数据结构

Effect 内置了对 ChunkHashSetHashMap 这类不可变数据结构的支持。这些数据结构保证一旦创建,其值就无法被修改,有助于写出更安全、更可预测的代码。相比之下,fp-ts 没有对这类数据结构的内置支持,只提供了为 SetMap 等标准数据类型补充 API 的模块。这些模块虽然有用,但它们并不具备 Effect 内置的不可变数据结构所提供的同等性能优化与专用操作。

不可变数据结构带来多项好处,包括:

  • 不可变性:不可变数据结构在创建之后无法被修改。这一性质消除了意外修改的风险,也让并发编程更安全。

  • 可预测性:使用不可变数据结构时,你可以确信它们的值不会意外改变。这种可预测性简化了对代码行为的推理,也减少了由可变状态引发的 bug。

  • 共享与复用:不可变数据结构可以在程序的不同部分之间安全共享。由于它们无法被修改,你不需要创建防御性副本,从而带来更高效的内存使用和更好的性能。

Stream 处理

Effect 生态内置了对 Stream 处理的支持,让你可以处理数据流。Stream 处理是一个强大的概念,让你能以响应式、异步的方式高效地处理和转换持续不断的数据流。不过,fp-ts 并没有内置这一特性,它依赖 RxJS 这类外部库来处理 Stream 处理。