拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Effect v3→v4 迁移指南:从结构化子类型到 Yieldable 契约,告别隐式 Effect 转换

Effect v3→v4 迁移指南从结构化子类型到 Yieldable 契约告别隐式 Effect 转换【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code本指南以 Effect 官方 v3→v4 迁移文档 yieldable.md 为主线深入剖析 v4 用Yieldabletrait 取代「结构子类型」这一核心类型系统变更哪些类型仍然可以yield*、哪些类型必须改用显式模块函数以及为什么这一变更能消除一整类难以排查的静默 bug。读完本文你将掌握 v3 代码迁移到 v4 时关于Ref、Deferred、Fiber、Option、Config、Context.Service等类型的确切改写规则并理解其背后的源码级实现原理。背景v3 的结构化子类型带来的隐患在 v3 中许多类型都是Effect的结构化子类型structural subtype——它们在运行时携带Effect的类型 ID因此可以出现在任何期望Effect的位置上。这包括RefDeferredFiberFiberRefConfigOptionEitherContext.Tag这种方式固然方便却埋下了一类难以诊断的微妙 bug。因为这些类型就是Effect当你本意是传递其内部值时它们可能被静默地传给Effect组合子。典型的误用包括把Ref传给Effect.map结果读取的是 ref 内部的值而不是变换 ref 本身——往往不是预期行为数据结构中的Deferred被静默当作Effect处理引发意想不到的 awaitEffect.all接受一组Ref值后静默地逐一读取它们而不是抛出类型错误。v4 用Yieldabletrait 取代了这套子类型机制它是一个更窄的契约允许在生成器中使用yield*但不会让类型可赋值给Effect。这样既保留了生成器语法的便利又在其余场合强制显式转换。Yieldable接口更窄的契约迁移文档给出的Yieldable接口定义如下interface YieldableSelf, A, E never, R never { asEffect(): EffectA, E, R [Symbol.iterator](): EffectIteratorSelf }实现Yieldable的类型示例类型yield 行为Effect本身直接产生成功值Option产生值或失败于NoSuchElementErrorResult产生成功值或失败于错误Config产生配置值或失败于ConfigErrorContext.Service从环境中产生服务不再是Effect子类型、也不实现Yieldable的类型类型v4 正确用法Ref用Ref.get(ref)读取Deferred用Deferred.await(deferred)等待Fiber用Fiber.join(fiber)汇合源码中的落实迭代器协议是共同分母从本仓库 v4 源码看各 Yieldable 类型实际落实的运行时契约正是[Symbol.iterator]()。例如 Effect.ts 中Effect接口本身export interface Effectout A, out E never, out R never extends Pipeable, Inspectable { readonly [TypeId]: VarianceA, E, R [Symbol.iterator](): EffectIteratorEffectA, E, R [Unify.typeSymbol]?: unknown [Unify.unifySymbol]?: EffectUnifythis [Unify.ignoreSymbol]?: {} }配套的EffectIterator定义于 Effect.ts标注since 4.0.0其next返回IteratorResultT, SuccessT即yield 一个 Effect把成功类型回传给生成器export interface EffectIteratorT extends Effectany, any, any { next( ...args: ReadonlyArrayany ): IteratorResultT, SuccessT }Option也在 Option.ts 声明了自己的迭代器协议OptionIteratorL89-L101并配套提供Option.genContext.Reference即 v4 的服务引用在 Context.ts 实现了EffectIteratorReferenceShape。这些正是可以被yield*的类型级证据。更值得注意的是迁移文档之外v4 还将可 yield 能力扩展到了错误类型Cause.YieldableError定义于 Cause.ts其签名同样实现了[Symbol.iterator](): Effect.EffectIteratorEffect.Effectnever, this, neverNoSuchElementError、TimeoutError、IllegalArgumentError、ExceededCapacityError、AsyncFiberError、UnknownError均实现它。由 Data.ts 可见Data.Error与Data.TaggedError都extendsCause.YieldableError因此实例可以在Effect.gen内部被 yieldSchema.Error同样如此。这意味着一套统一的迭代器协议同时服务于值、配置、服务与错误。yield*依然可用Effect.gen中的yield*适用于任何Yieldable。按迁移文档的描述运行时在 yield 时会在内部调用.asEffect()完成到Effect的转换。import { Effect, Option } from effect // program 的类型是 Effectnumber, NoSuchElementError const program Effect.gen(function*() { // yield* 适用于 Yieldable 类型——与 v3 行为一致 const value yield* Option.some(42) return value // 42 })从源码看Effect.gen在 Effect.ts 提供两个重载一个是f: () GeneratorEff, AEff, never另一个支持以选项对象传入selfexport const gen: { Eff extends Effectany, any, any, AEff( f: () GeneratorEff, AEff, never ): Effect AEff, [Eff] extends [never] ? never : [Eff] extends [Effectinfer _A, infer E, infer _R] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effectinfer _A, infer _E, infer R] ? R : never Self, Eff extends Effectany, any, any, AEff( options: { readonly self: Self }, f: (this: Self) GeneratorEff, AEff, never ): Effect... }顺带一提v4 中向Effect.gen传self也必须改为选项对象语法Effect.gen({ self: this }, function*() { ... })详见同目录的 generators.md。Effect 组合子需要显式.asEffect()v3 中因为Yieldable类型是Effect的子类型可以直接传给 Effect 组合子v4 中则必须显式调用.asEffect()转换。v3——Option是 Effect 子类型可以直接编译import { Effect, Option } from effect // Optionnumber 可赋值给 Effectnumber, NoSuchElementError const program Effect.map(Option.some(42), (n) n 1)v4——Option不是 Effect必须显式转换import { Effect, Option } from effect // Option 是 Yieldable 但不是 Effect —— 使用 .asEffect() const program Effect.map(Option.some(42).asEffect(), (n) n 1) // 或者更惯用的写法使用生成器 const program2 Effect.gen(function*() { const n yield* Option.some(42) return n 1 })除了.asEffect()v4 还提供Effect.effectify见 Effect.ts对应 v3 的effect/platform/Effectify用于把普通函数/回调风格 API 包装为Effect。实践中优先考虑生成器写法而非手动.asEffect()生成器同时保留了yield*的平铺语义与错误通道的类型推导可读性和类型安全性都更好。不再作为 Effect 子类型的三种类型以下是迁移影响最集中的三类类型。它们不再是Effect的子类型必须改用对应的模块函数。Ref用Ref.get读取v3——Ref继承EffectAyield 即取当前值import { Effect, Ref } from effect const program Effect.gen(function*() { const ref yield* Ref.make(0) const value yield* ref // Ref 是 Effectnumber })v4——Ref是纯值用Ref.getimport { Effect, Ref } from effect const program Effect.gen(function*() { const ref yield* Ref.make(0) const value yield* Ref.get(ref) })源码印证v4 的Ref接口Ref.ts只是一个携带MutableRef的普通可管道值不再声明任何[Symbol.iterator]export interface Refin out A extends Ref.VarianceA, Pipeable { readonly ref: MutableRef.MutableRefA }相应地Ref.makeL173返回EffectRefA创建仍是效果Ref.getL200返回Effect.sync(() self.ref.current)读取才是效果。Ref的操作集get、set、getAndSet、getAndUpdate、getAndUpdateSome等全部以返回Effect的模块函数形态提供这正是持有 ref与读取 ref 的效果被类型系统彻底区分开的表现。Deferred用Deferred.await等待v3——Deferred继承EffectA, Eyield 时解析完成import { Deferred, Effect } from effect const program Effect.gen(function*() { const deferred yield* Deferred.makestring, never() const value yield* deferred // Deferred 是 Effectstring })v4——Deferred是纯值用Deferred.awaitimport { Deferred, Effect } from effect const program Effect.gen(function*() { const deferred yield* Deferred.makestring, never() const value yield* Deferred.await(deferred) })源码印证Deferred接口Deferred.ts同样只是Deferred.VarianceA, E Pipeable的纯值无迭代器官方 JSDoc 示例L47、L162均以Deferred.await(deferred)作为标准用法Deferred.makeL171返回EffectDeferredA, E。Fiber用Fiber.join汇合v3——Fiber继承EffectA, Eyield 时 joinimport { Effect, Fiber } from effect const program Effect.gen(function*() { const fiber yield* Effect.fork(task) const result yield* fiber // Fiber 是 EffectA, E })v4——Fiber是纯值用Fiber.joinimport { Effect, Fiber } from effect const program Effect.gen(function*() { const fiber yield* Effect.forkChild(task) const result yield* Fiber.join(fiber) })源码印证Fiber接口Fiber.ts携带id、currentOpCount、getRef等字段是纯值而非 EffectFiber.joinL279返回EffectA, E等待并取成功值而Fiber.await返回EffectExitA, E保留完整退出信息成功时得到Exit.succeed(...)。此外 v4 生成子纤程的惯用 API 是Effect.forkChild定义于 Effect.ts更贴合结构化并发语义——选择join还是await取决于你需要值还是完整退出结果。为什么这样改类型系统不再说谎v3 的子类型方案意味着类型系统无法区分我有一个Ref与我有一个读取Ref的Effect。这种歧义催生了以下难以定位的 bugEffect.map误读 ref把Ref传给Effect.map会读取 ref 当前值并对其变换而不是变换 ref 本身——绝大多数情况下并非本意数据结构中的隐式 await放在数据结构里的Deferred可能被静默当作Effect处理在意外时机触发等待Effect.all静默读取Effect.all曾接受一组Ref并静默读取全部值而不是产生类型错误——直到运行时行为暴露问题。Yieldabletrait 的引入同时满足两个目标保留生成器中yield*的平铺语法——它在生成器内仍可无缝解包Option、Result、Config、服务与各种 yieldable 错误在其他所有场合强制显式转换——任何把 yieldable 值直接塞给 Effect 组合子的企图都会在编译期被拦截而非在运行时悄悄产生不符合预期的行为。结合 v3-to-v4.md 提供的完整导入映射例如effect/Either→effect/Result、effect/FiberRef→effect/References、effect/TRef→effect/TxRef等可以进一步确认v4 在收窄可 yield契约的同时还伴随大量模块重命名与合并两者叠加构成了这次迁移的类型层面主线。迁移自查清单把 v3 代码升级到 v4 时建议按以下顺序排查 yieldable 相关代码搜索yield*后的裸值yield* ref、yield* deferred、yield* fiber这类写法必须改写为yield* Ref.get(ref)、yield* Deferred.await(deferred)、yield* Fiber.join(fiber)。检查传给组合子的首参Effect.map(x, f)、Effect.all([...])、Effect.flatMap(x, f)等调用中若x是Option、Result、Config、服务等 yieldable 类型改用.asEffect()显式转换或整体重写为Effect.gen生成器。留意错误类型NoSuchElementError、TimeoutError等继承Cause.YieldableError的类型Cause.ts依旧可以 yield这是 v4 有意的能力保留不需要改写。区分Fiber.join与Fiber.awaitjoin 得值、await 得Exit语义不同不要混用。生成器self参数Effect.gen(this, f)改为Effect.gen({ self: this }, f)见 generators.md。迁移文档目录.repos/effect-smol/migration还提供了与本主题配套的专项指南可作为深入阅读完整 API 导入映射见 v3-to-v4.md服务定义从Context.Tag迁移到Context.Service见 services.md错误处理、作用域Scope、纤程forking、FiberRef等主题均有独立文档cause.md、error-handling.md、scope.md、forking.md、fiberref.md。核心结论一句话v4 的Yieldable用迭代器协议 显式转换取代了Effect 子类型这一隐式通道——yield*的便利被完整保留而隐式传递 yieldable 值给组合子的一切路径都被类型系统关闭这正是本次迁移为消除静默错误所付出的设计取舍。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门