关于 effect-advanced
effect-advanced 是构建生产级 Effect-TS 应用的规范型技能,聚焦类型化错误、依赖注入、资源作用域、并发及常见陷阱,而非完整 API 文档。
主要功能
- 01Effect.gen 业务逻辑规范
- 02类型化错误处理模式
- 03Service 与 Layer 架构
- 04作用域资源管理
- 05高级并发 API
- 06陷阱与参考指南
适用场景
- 01组织 Effect-TS 应用服务
- 02处理类型化失败与缺陷
- 03组合依赖 Layer
- 04管理资源生命周期与并发
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trancong12102/ agentskills --skill effect-advanced安装后在 Claude Code 中输入/effect-advanced确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/concurrency.mddependency-injection.mderror-handling.mdplatform.mdresource-management.mdschema.mdstream.mdtesting.md详细介绍
Effect 高级用法:模式、约定与陷阱
本技能定义了构建生产级 Effect-TS 应用时应遵循的规则、约定和架构决策。它有意采用明确的立场,以避免常见陷阱并强制应用可扩展的模式。
如需详细的 API 文档,请使用其他合适的工具(文档查询、网络搜索等)——本技能侧重于以惯用方式使用 Effect 的方法和原因,而非完整的 API。
核心约定
使用 Effect.gen 编写业务逻辑
生成器读起来就像同步代码;对于超出简单组合的逻辑,强烈建议使用生成器,而不是冗长的 .pipe / .flatMap 链:
const program = Effect.gen(function* () {
const config = yield* ConfigService;
const user = yield* UserRepo.findById(config.userId);
return user;
});
仅在数据转换流水线和简短的组合器链中使用 pipe。
切勿抛出异常——使用 Effect 的错误通道
| 不要使用…… | 应使用 |
|---|---|
throw new Error() | Effect.fail(new MyError()) |
对 Promise 使用 try/catch | Effect.tryPromise({ try, catch }) |
| 回调 API | Effect.async((resume) => ...) |
| 不可恢复的崩溃 | Effect.die(defect) |
优先使用函数,而非方法
为提升可组合性和摇树优化效果,优先使用 Effect.map(e, f),而不是 e.pipe(Effect.map(f))。对于应用,扁平导入(import { Effect } from "effect")没有问题;对于库,命名空间导入(import * as Effect from "effect/Effect")更合适。
@effect/schema 已弃用
Schema 已合并到核心 effect 中。请直接从 "effect" 导入:
import { Schema } from "effect";
// NOT: import { Schema } from "@effect/schema"
在生产环境中使用 NodeRuntime.runMain
Effect.runPromise 无法妥善处理 SIGINT / SIGTERM:
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));
错误处理理念
失败与缺陷——根本区别
| 方面 | 失败(预期) | 缺陷(意外) |
|---|---|---|
| API | Effect.fail(new MyError()) | Effect.die(new Error()) |
| 类型通道 | 在 E 中跟踪 | 永远不会出现在 E 中(never) |
| 恢复方式 | catchTag、catchAll、retry | 仅在系统边界处处理 |
| 判断原则 | 你打算在调用处处理它 | 错误或不可能的状态 |
始终使用带标签的错误
普通 Error 或字符串形式的失败无法发挥 Effect 类型化错误通道的价值:
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string;
}> {}
// Tagged errors are yieldable — no Effect.fail wrapper needed
const program = Effect.gen(function* () {
const user = yield* db.findUser(id);
if (!user) yield* new UserNotFound({ id });
return user;
});
catchAll 不会捕获缺陷
这是错误处理中最常见的错误:
Effect.catchAll(program, handler); // catches E only — NOT defects
Effect.catchAllCause(program, handler); // catches everything (E + defects + interrupts)
仅在系统边界处使用 catchAllCause / catchAllDefect(顶层错误处理器、HTTP 响应映射器)。
依赖注入架构
Service → Layer → Provide(一次即可)
1. Define services with Context.Tag → "what do I need?"
2. Implement via Layers → "how is it built?"
3. Provide once at entry point → "wire it all together"
Service 方法的 R 必须为 never
依赖应属于 Layer 组合,而非方法签名:
// WRONG: leaks dependency to callers
findById: (id: string) => Effect.Effect<User, UserNotFound, Database>;
// RIGHT: Database is wired in the Layer
findById: (id: string) => Effect.Effect<User, UserNotFound>;
Layer 组合——了解这些运算符
| 操作 | 使用场景 | 行为 |
|---|---|---|
Layer.merge(A, B) | 独立服务 | 并发构建二者 |
Layer.provide(downstream, upstream) | A 为 B 提供依赖 | 先构建 upstream |
Layer.fresh(layer) | 强制创建新实例 | 绕过记忆化 |
重要:Layer.merge 不会按顺序构建。如果 B 依赖 A,请使用 Layer.provide,不要使用 Layer.merge。
在入口点调用一次 Effect.provide
分散的 provide 调用会造成隐藏依赖和 Layer 重复:
// WRONG: provide scattered throughout codebase
const getUser = UserRepo.findById(id).pipe(Effect.provide(DbLayer));
// RIGHT: compose and provide once
const main = program.pipe(Effect.provide(AppLayer));
NodeRuntime.runMain(main);
资源与作用域规则
使用 acquireRelease 时必须使用 Effect.scoped
忘记使用 Effect.scoped 是资源管理中最常见的陷阱——资源会不断累积,直到程序退出:
// WRONG: scope never closes, connection leaks
const result = yield * getDbConnection;
// RIGHT: scope closes when block completes
const result =
yield *
Effect.scoped(
Effect.gen(function* () {
const conn = yield* getDbConnection;
return yield* conn.query("SELECT 1");
}),
);
发布终结器始终会运行
无论成功、失败还是中断,都保证运行。终结器会接收
Exit 值,以便有条件地执行清理。
在一个作用域中管理多个资源
Effect.scoped(
Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(openConn(), closeConn);
const file = yield* Effect.acquireRelease(openFile(), closeFile);
// both released when scope closes, in REVERSE acquisition order
}),
);
并发模型
优先使用高级 API,而非原始 fork
| API | 使用场景 |
|---|---|
Effect.all([], { concurrency: N }) | 有界并行执行 |
Effect.forEach(items, fn, { concurrency: N }) | 工作池模式 |
Effect.race(a, b) | 谁先完成就采用谁,其他任务会被中断 |
Effect.timeout(e, dur) | 为任意效果设置截止时间 |
只有在高级 API 无法满足需求时,才使用 Effect.fork / Fiber。
Fork 变体 — 了解其生命周期
| 函数 | 作用域 | 清理方式 |
|---|---|---|
Effect.fork | 父级作用域 | 随父级自动中断 |
Effect.forkDaemon | 全局作用域 | 不会自动清理 — 必须手动清理 |
Effect.forkScoped | 最近的 Scope | 与资源生命周期绑定 |
注意: 如果忘记中断 forkDaemon,会导致 fiber 泄漏。
常见陷阱
-
未执行的效果 — 创建 Effect 却不 yield 或运行它,是一种静默错误。在生成器中调用
Effect.log("msg"),除非使用yield*,否则什么也不会发生。 -
catchAll无法捕获缺陷 — 在系统边界使用catchAllCause,以完整了解失败情况。 -
缺少
Effect.scoped— 不设置作用域边界就使用acquireRelease,会导致资源一直泄漏到程序退出。 -
零散使用
Effect.provide— 组合所有层,并在入口点统一提供。 -
对重载函数使用 Point-free 写法 —
Effect.map(myOverloadedFn)会静默擦除泛型。请使用显式 lambda:Effect.map((x) => myOverloadedFn(x))。 -
多次调用
Effect.async的 resume — resume 必须且只能调用一次。多次调用会导致未定义行为。 -
orDie会掩盖错误 — 它会将类型化失败转换为非类型化缺陷。应当妥善处理错误。 -
用
Layer.merge处理有依赖关系的服务 — merge 不会按顺序构造。某个层需要另一个层的输出时,应使用Layer.provide。 -
Fiber.join与Fiber.await— 在某些边界情况下,join可能导致终结器过早执行。涉及资源安全时,优先使用await。 -
对无限流使用
runCollect— 如果之前没有调用take,绝不要调用它。操作永远不会终止,还会消耗无界内存。 -
在作用域测试中使用
it.effect— 需要Scope的效果必须使用it.scoped,而不是it.effect,否则会出现类型错误。
参考文件
处理特定问题时,请阅读相关参考文件:
| 文件 | 阅读时机 |
|---|---|
references/error-handling.md | 带标签的错误、Cause、缺陷恢复、错误映射模式 |
references/dependency-injection.md | 服务、Layers、组合、记忆化、provide 模式 |
references/concurrency.md | Fibers、fork 变体、Deferred、Semaphore、结构化并发 |
references/resource-management.md | Scope、acquireRelease、Layer 资源、fork 与 scope 的交互 |
references/schema.md | Schema 定义、转换、品牌类型、递归 Schema |
references/stream.md | Stream 运算符、分块、背压、带资源的 Stream |
references/testing.md | @effect/vitest、TestClock、Layer 模拟、Config 模拟 |
references/platform.md | HTTP 客户端、FileSystem、Command、运行时、框架集成 |