Chapter 06

自测题库

05 章用一个完整项目把概念整合起来。这一章是检验——三层梯度共 16 题:概念层(01)、原理层(02)、应用判别层(03–05 综合)。所有答案集中在文末一个折叠块里。先做完再对照,瞄一眼答案等于把这一章作废。

怎么用这一章

  • 合上前面所有章节,凭记忆作答——卡壳的地方就是没真正学透的地方
  • 概念层卡 = 回 01;原理层卡 = 回 02;判别层卡 = 回 03–05
  • 判别层是重点:能选对方案并说出理由,才算真学会
概念层 · 回忆 Effect 是什么 · 三通道 · 创建/运行/组合 原理层 · 理解 错误追踪 · 调度 · 中断 为什么这样 判别层 · 迁移 该用哪个方案 难 易
图 6.0题库的三层梯度。注意:越往上越窄、越难——大多数人卡在判别层(迁移),因为它要求你在两个都"会用"的方案间选对一个,这正是 fluency(顺)和 mastery(会)的分界。

6.1概念层(对应 01 章)

  1. Effect 和 Promise 在"何时执行"上的根本区别是什么?同一个 effect 值 run 两次会怎样?提示 01 §1.1
  2. Effect<A, E, R> 三个参数各代表什么?E=never 和 R=never 分别意味着什么?提示 01 §1.2
  3. Effect.sync 和 Effect.try 的区别?什么时候必须用 try?提示 01 §1.3
  4. yield* 在 Effect.gen 里扮演什么角色?和 await 的相同与不同?提示 01 §1.5
  5. 一个 effect 的 R 通道里有 Database,要让它能 runPromise,缺哪一步?提示 01 §1.7

6.2原理层(对应 02 章)

  1. 串联两个会失败的 effect,E 通道怎么变?用 catchTag 处理其中一种后又怎么变?提示 02 §2.2
  2. "失败"和"缺陷"的区别是什么?为什么 catchAll 抓不到缺陷?提示 02 §2.2
  3. Cause 的 Parallel 形态能做到、而 try/catch 做不到的是什么?提示 02 §2.2
  4. 为什么 fiber 的中断"只发生在效应边界",这让中断有了什么性质?提示 02 §2.4
  5. 结构化并发对"fiber 泄漏"给了什么默认保证?怎么打破它?提示 02 §2.5
  6. Layer 的"按引用记忆化"是什么意思?它如何导致"两个实例"的陷阱?提示 02 §2.3

6.3应用判别层(综合 03–05)

每题给一个场景,在两个方案间选一个并说理由。这是迁移训练,不是回忆。

  1. 场景:批量调用 20 个外部 API 汇总数据,个别失败可接受,要尽量多拿到结果。你用 Effect.all(tasks) 还是 Effect.all(tasks.map(Effect.exit))?并发度怎么设?
  2. 场景:一个解析函数内部调用 JSON.parse,你希望解析失败能被上层类型化地处理。用 Effect.sync 还是 Effect.try?后续用 catchAll 还是 catchTag?
  3. 场景:一个写一次性运维脚本的同事问你"要不要上 Effect"。脚本就是顺序调三个 API、打印结果。你怎么建议?依据 04 章哪一节?
  4. 场景:服务里有个数据库连接池,多个 handler 共用。你把它做成 Layer。怎么保证全程只有一个池实例?哪种写法会意外造出两个?
  5. 场景:调度器要求"用户取消请求时,所有在飞的子任务停止并释放连接"。你靠 AbortController 手动管,还是靠 Effect 的中断 + acquireRelease?为什么后者更省心?

6.4亲手画一张图

亲手画一张图

不看教程,在纸上画出 Effect<A, E, R> 的"三通道流":一个 effect 从定义到 run,A / E / R 三条信息分别在何时被填上、被消除。只画 3 条线 + 4 个关键点(定义、组合累积、provide/catch 消除、run)。画完回到 01 §1.2 与 02 §2.2 对照——你的图里,"catchTag 让 E 变小"和"provide 让 R 变 never"这两个"减法"画出来了吗?

6.5答案(先做完再展开)

展开全部答案

概念层

  1. Promise 构造即执行(热);Effect 是惰性值,run* 才执行(冷)。同一个 effect 值 run 两次 = 副作用执行两次(重试就靠这个)。
  2. A=成功值、E=可能的失败、R=需要的依赖。E=never=不会失败;R=never=无未满足依赖、可以直接 run。
  3. sync 包不会抛的副作用;try 包可能抛的同步调用并把异常转进 E 通道。会抛的代码(如 JSON.parse)必须用 try,否则异常变缺陷绕过类型化处理。
  4. yield* 取出上一步 effect 的结果再继续,形似 await。相同:都表达"等这步结果"。不同:await 即时执行;yield* 只把这步组合进描述,整个 gen 块仍是未运行的值。
  5. 缺一步 Effect.provide(一个 Database 的 Layer),把 R 从 Database 消成 never。

原理层

  1. 串联取并集:E = E1 | E2(累积)。catchTag 处理一种后做减法,把那一项从并集去掉;全处理完 E 变 never。
  2. 失败=Effect.fail 产生、预期可恢复、进 E 通道;缺陷=throw/die 的 bug、不进 E。catchAll 只作用于 E 通道,故抓不到缺陷(要用 catchAllCause)。
  3. 同时保留多个并发失败。两个并行分支都炸,Parallel 把两个都留着;try/catch 只能抓到一个异常,另一个被吞。
  4. 因为 runtime 掌控每一步、只在步与步之间插入中断点,绝不在同步块中途打断 → 中断安全(无撕裂状态、不需要锁)、且便宜。代价:CPU 密集的同步块不会让出也不响应中断,直到撞上边界。
  5. 默认保证:子 fiber 随父 fiber 终止而终止,"不会有 fiber 在你不知情时还在跑"。打破:用 forkDaemon(全局作用域)或 forkScoped(挂到外层 Scope)让它活得更久。
  6. 同一个 layer 引用在一次构建中只构建一次、共享实例。把工厂 makeLayer() 调两次得到两个不同引用 = 两次构建 = 两个实例。修法:定义成一个 const 共享。

应用判别层

  1. 用 Effect.all(tasks.map(Effect.exit))——个别失败可接受、要尽量多拿结果,需要失败隔离(P10)。并发度设具体数字(如 5)限流,不用 "unbounded" 打爆 20 个下游(P8)。
  2. 用 Effect.try(把 JSON.parse 的抛出转成 E 通道的 ParseError),后续用 catchTag("ParseError", ...) 精确处理。用 sync 会让异常变缺陷(P4),用 catchAll 会顺手吞掉别的错误(P5)。
  3. 建议不上 Effect,直接 Promise + try/catch。依据 04 §4.5:一次性脚本、简单顺序逻辑,Effect 的学习曲线不划算。
  4. 把 Layer 定义成模块级 const DbPool = Layer.effect(...),所有 handler 都 provide 这同一个引用——按引用记忆化保证单例。意外造两个的写法:把它包成工厂 makeDbPool() 在多处各调一次(P7)。
  5. 靠 Effect 中断 + acquireRelease。AbortController 只翻标志位、要手动轮询、不保证清理、不自动向下传播;Effect 的中断沿结构化并发自动传到所有子 fiber,且 finalizer(释放连接)必跑(§2.5 + 表 2.5)。

下一步学什么

  • Schema——边界处把 unknown 解析成类型安全数据,错误进 E 通道(接续本教程的 decode 校验 那一格)。
  • Stream——把单值 effect 扩展成可背压、可中断的多值流。
  • @effect/platform——跨运行时的 HTTP server / client / 文件抽象,把调度器接到真实网络。
  • 可观测性——内建 logging / tracing / metrics,在生产看见 fiber 在做什么。
  • v4 迁移——v4 稳定后对照官方 v3→v4 映射;核心心智模型不变。