Effect 资源清理的三种写法怎么选?
Effect 里要在“用完一定清理”的场景下写资源代码,至少有三个入口:acquireRelease、acquireUseRelease、addFinalizer。选错的后果不是报错,而是资源的生命周期比你以为的长,或者多包了一层不必要的 Scope。
简单的选法是:资源只在一个地方用,选 acquireUseRelease;资源要跨多步、跨函数传递,选 acquireRelease 加 Effect.scoped;只想在当前 Scope 里顺手加一个收尾动作,选 addFinalizer。
三者共用同一套机制:Scope 关闭时,已登记的清理动作后进先出地执行,成功、失败、中断都一样。如果你还不清楚这套机制,先读 Effect 怎么保证资源一定被释放。下面分别看每个入口。
资源只用一次,用什么?
Effect.acquireUseRelease(acquire, use, release) 是三段式,获取、使用、释放写在一个表达式里,不需要外层 Effect.scoped。资源只在这一个使用点用到时,它最直观,生命周期天然就是这一个表达式。
资源要跨多步传递,用什么?
用 Effect.acquireRelease,它只写获取与释放两部分:
import { Effect } from "effect"
interface Conn {
readonly query: (sql: string) => Promise<unknown>
readonly close: () => Promise<void>
}
declare const open: () => Promise<Conn>
const connection = Effect.acquireRelease(
Effect.promise(() => open()),
(conn) => Effect.promise(() => conn.close())
)结果的 R 里会带 Scope.Scope,表示资源需要有人划定生命周期。用 Effect.scoped 包住使用它的那段流程,就划定了范围。这个写法的好处是,“返回一个已打开连接”的函数不再把释放责任甩给调用方:责任已经和资源一起登记在 Scope 里。
只想加一个收尾动作,用什么?
Effect.addFinalizer 只登记清理动作,不关联获取。需要在当前 Scope 里顺便加一个收尾,比如打一条日志、通知对端,就用它。finalizer 会收到 Exit,能区分是成功、失败还是中断:
import { Effect } from "effect"
declare const job: Effect.Effect<string>
const withNotice = Effect.gen(function*() {
yield* Effect.addFinalizer((exit) =>
Effect.log(`job finished: ${exit._tag}`)
)
return yield* job
}).pipe(Effect.scoped)Effect.ensuring 和 Effect.onExit 能替代它们吗?
不能当资源管理用。它们适合“无论如何都做一件事”,但不是“获取与释放成对”的表达。只要涉及获取了一个需要归还的东西,就应该用 acquireRelease 这一族。
本文整理自一篇 Effect 教程章节,没有记录 effect 的版本号。文中的写法和选择建议按原章节整理,我没有在本地逐一运行这些示例,使用前请对照你所用版本的文档。