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 的版本号。文中的写法和选择建议按原章节整理,我没有在本地逐一运行这些示例,使用前请对照你所用版本的文档。