Chapter 06
自测题库
05 章用一个完整项目把概念整合起来。这一章是检验——三层梯度共 16 题:概念层(01)、原理层(02)、应用判别层(03–05 综合)。所有答案集中在文末一个折叠块里。先做完再对照,瞄一眼答案等于把这一章作废。
怎么用这一章
- 合上前面所有章节,凭记忆作答——卡壳的地方就是没真正学透的地方
- 概念层卡 = 回 01;原理层卡 = 回 02;判别层卡 = 回 03–05
- 判别层是重点:能选对方案并说出理由,才算真学会
6.1概念层(对应 01 章)
- Effect 和 Promise 在"何时执行"上的根本区别是什么?同一个 effect 值 run 两次会怎样?提示 01 §1.1
Effect<A, E, R>三个参数各代表什么?E=never和R=never分别意味着什么?提示 01 §1.2Effect.sync和Effect.try的区别?什么时候必须用try?提示 01 §1.3yield*在Effect.gen里扮演什么角色?和await的相同与不同?提示 01 §1.5- 一个 effect 的 R 通道里有
Database,要让它能runPromise,缺哪一步?提示 01 §1.7
6.2原理层(对应 02 章)
- 串联两个会失败的 effect,E 通道怎么变?用
catchTag处理其中一种后又怎么变?提示 02 §2.2 - "失败"和"缺陷"的区别是什么?为什么
catchAll抓不到缺陷?提示 02 §2.2 Cause的Parallel形态能做到、而try/catch做不到的是什么?提示 02 §2.2- 为什么 fiber 的中断"只发生在效应边界",这让中断有了什么性质?提示 02 §2.4
- 结构化并发对"fiber 泄漏"给了什么默认保证?怎么打破它?提示 02 §2.5
- Layer 的"按引用记忆化"是什么意思?它如何导致"两个实例"的陷阱?提示 02 §2.3
6.3应用判别层(综合 03–05)
每题给一个场景,在两个方案间选一个并说理由。这是迁移训练,不是回忆。
- 场景:批量调用 20 个外部 API 汇总数据,个别失败可接受,要尽量多拿到结果。你用
Effect.all(tasks)还是Effect.all(tasks.map(Effect.exit))?并发度怎么设? - 场景:一个解析函数内部调用
JSON.parse,你希望解析失败能被上层类型化地处理。用Effect.sync还是Effect.try?后续用catchAll还是catchTag? - 场景:一个写一次性运维脚本的同事问你"要不要上 Effect"。脚本就是顺序调三个 API、打印结果。你怎么建议?依据 04 章哪一节?
- 场景:服务里有个数据库连接池,多个 handler 共用。你把它做成
Layer。怎么保证全程只有一个池实例?哪种写法会意外造出两个? - 场景:调度器要求"用户取消请求时,所有在飞的子任务停止并释放连接"。你靠
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答案(先做完再展开)
展开全部答案
概念层
- Promise 构造即执行(热);Effect 是惰性值,
run*才执行(冷)。同一个 effect 值 run 两次 = 副作用执行两次(重试就靠这个)。 - A=成功值、E=可能的失败、R=需要的依赖。
E=never=不会失败;R=never=无未满足依赖、可以直接 run。 sync包不会抛的副作用;try包可能抛的同步调用并把异常转进 E 通道。会抛的代码(如JSON.parse)必须用try,否则异常变缺陷绕过类型化处理。yield*取出上一步 effect 的结果再继续,形似await。相同:都表达"等这步结果"。不同:await即时执行;yield*只把这步组合进描述,整个gen块仍是未运行的值。- 缺一步
Effect.provide(一个 Database 的 Layer),把 R 从Database消成never。
原理层
- 串联取并集:
E = E1 | E2(累积)。catchTag处理一种后做减法,把那一项从并集去掉;全处理完 E 变never。 - 失败=
Effect.fail产生、预期可恢复、进 E 通道;缺陷=throw/die的 bug、不进 E。catchAll只作用于 E 通道,故抓不到缺陷(要用catchAllCause)。 - 同时保留多个并发失败。两个并行分支都炸,
Parallel把两个都留着;try/catch只能抓到一个异常,另一个被吞。 - 因为 runtime 掌控每一步、只在步与步之间插入中断点,绝不在同步块中途打断 → 中断安全(无撕裂状态、不需要锁)、且便宜。代价:CPU 密集的同步块不会让出也不响应中断,直到撞上边界。
- 默认保证:子 fiber 随父 fiber 终止而终止,"不会有 fiber 在你不知情时还在跑"。打破:用
forkDaemon(全局作用域)或forkScoped(挂到外层 Scope)让它活得更久。 - 同一个 layer 引用在一次构建中只构建一次、共享实例。把工厂
makeLayer()调两次得到两个不同引用 = 两次构建 = 两个实例。修法:定义成一个const共享。
应用判别层
- 用
Effect.all(tasks.map(Effect.exit))——个别失败可接受、要尽量多拿结果,需要失败隔离(P10)。并发度设具体数字(如 5)限流,不用"unbounded"打爆 20 个下游(P8)。 - 用
Effect.try(把JSON.parse的抛出转成 E 通道的ParseError),后续用catchTag("ParseError", ...)精确处理。用sync会让异常变缺陷(P4),用catchAll会顺手吞掉别的错误(P5)。 - 建议不上 Effect,直接 Promise + try/catch。依据 04 §4.5:一次性脚本、简单顺序逻辑,Effect 的学习曲线不划算。
- 把 Layer 定义成模块级
const DbPool = Layer.effect(...),所有 handler 都 provide 这同一个引用——按引用记忆化保证单例。意外造两个的写法:把它包成工厂makeDbPool()在多处各调一次(P7)。 - 靠 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 映射;核心心智模型不变。