从 ZIO 转向 Effect
Effect 与 ZIO 之间的关键差异。
如果你是从 ZIO 转向 Effect 的开发者,有一些差异需要留意。
环境
在 Effect 中,我们把运行一段 effect 工作流所需的环境表示为服务的并集(union):
示例(使用服务的并集定义环境)
import { Effect } from "effect"
interface IOError {
readonly _tag: "IOError"
}
interface HttpError {
readonly _tag: "HttpError"
}
interface Console {
readonly log: (msg: string) => void
}
interface Logger {
readonly log: (msg: string) => void
}
type Response = Record<string, string>
// `R` is a union of `Console` and `Logger`
type Http = Effect.Effect<Response, IOError | HttpError, Console | Logger>
对于从 ZIO 过来的人,这可能有些令人困惑:在 ZIO 中,环境被表示为服务的交集(intersection):
type Http = ZIO[Console with Logger, IOError, Response]
设计缘由
使用并集来表示 Effect 工作流所需环境,其缘由归根结底在于我们希望去掉作为环境中服务包装器的 Has(这与 ZIO 2.0 中达成的目标类似)。
为了能从 Effect 中去掉 Has,考虑到 TypeScript 是结构化类型系统,我们必须更多地从结构的角度来思考。在 TypeScript 中,如果你有一个类型 A & B,而 A 与 B 之间存在结构冲突,那么类型 A & B 就会归约为 never。
示例(交叉类型的冲突)
interface A {
readonly prop: string
}
interface B {
readonly prop: number
}
// @errors: 2322
const ab: A & B = {
prop: "",
}
在早期的 Effect 版本中,人们用交叉类型来表示包含多个服务的环境。使用交叉类型(即 A & B)的问题在于:环境中可能存在多个服务,它们拥有同名的方法和属性。为了解决这个问题,我们把服务包装进 Has 类型(类似于 ZIO 1.0),于是你的环境中就会出现 Has<A> & Has<B>。
在 ZIO 2.0 中,ZIO 类型(表示环境)的_逆变(contravariant)_ R 类型参数变成了完全幻影(phantom)的,从而可以移除 Has 类型。这显著提升了类型签名的清晰度,也为新用户消除了又一个「绊脚石」。
为了在 Effect 中顺利移除 Has,我们必须考虑环境中的类型如何组合。按照组合规则,就可赋值性而言,以交叉类型(即 &)组合起来的逆变参数,等价于以并集(即 |)组合起来的协变参数。基于这一点,我们决定与 ZIO 分道扬镳,把 R 类型参数设为_协变(covariant)_的,因为当 A 与 B 存在冲突时,A | B 不会归约为 never。
沿用上面的例子:
interface A {
readonly prop: string
}
interface B {
readonly prop: number
}
// ok
const ab: A | B = {
prop: "",
}
把 R 表示为一个协变类型参数,其中承载 Effect 工作流所需的服务的并集,这使我们得以去掉对 Has 的要求。
类型别名
在 Effect 中,不像 ZIO 那样有 UIO、URIO、RIO、Task、IO 这类预定义的类型别名。
原因在于,类型别名一旦被组合就会丢失,除非你为每一个函数都维护多套签名,否则它们多少有些无用。在 Effect 中,我们选择不走这条路。相反,我们用 never 类型来表示未被使用的类型。
值得一提的是,「类型别名更容易理解」这种感觉往往只是一种错觉。在 Effect 中,显式的写法 Effect<A> 清楚地表明只使用了类型 A。而使用 RIO<R, A> 这样的类型别名时,就会冒出关于类型 E 的疑问:它是 unknown 吗?还是 never?要记住这类细节并不容易。