Runtime 入门
了解 Effect 的运行时系统如何以灵活高效的方式执行并发程序、管理资源并处理配置。
Effect 的运行时系统(runtime system)把 Effect<A, E, R> 这样的蓝图变成真正运行的程序:它提供 R 所代表的需求,逐步执行每一个步骤,并产出结果。
Effect.run* 系列函数(Effect.runPromise、Effect.runFork、Effect.runSync 等)会利用 effect 的运行时系统立即执行它。如果你已经拥有满足该 effect 需求的 Context<R>,那么 Effect.run*With 变体(Effect.runPromiseWith、Effect.runForkWith、Effect.runSyncWith)可以让你直接带着该 context 运行。若你想要一个可复用、顶层的执行配置,请使用 ManagedRuntime(本页稍后会介绍)。
什么是运行时系统?
当我们编写 Effect 程序时,我们会用各种构造器和组合子来构造一个 Effect。本质上,我们是在创建一份程序的蓝图。Effect 只是一个描述并发程序执行过程的数据结构。它表现为一种树状结构,把各种原语组合在一起,定义该 effect 应该做什么。
然而,这个数据结构本身不会执行任何动作,它仅仅是对一个并发程序的描述。
要执行这个程序,就需要 Effect 运行时系统登场。Effect.run* 系列函数(例如 Effect.runPromise、Effect.runFork)负责接收这份蓝图并执行它。
当运行时系统运行一个 effect 时,它会创建一个根 Fiber,并用以下内容初始化它:
- 初始 context
- 初始的 Fiber 局部状态
- 初始 effect
然后它启动一个循环,逐步执行 Effect 所描述的指令。
你可以把运行时看作这样一个系统:它接收一个 Effect<A, E, R> 及其关联的 context Context<R>,并产出 Exit<A, E> 结果。
┌────────────────────────────────┐
│ Context<R> + Effect<A, E, R> │
└────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ Effect Runtime System │
└────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ Exit<A, E> │
└────────────────────────────────┘
运行时系统肩负着许多职责:
| 职责 | 说明 |
|---|---|
| 执行程序 | 运行时必须循环执行 effect 的每一个步骤,直到程序完成。 |
| 处理错误 | 它同时处理执行过程中出现的预期错误与意外错误。 |
| 管理并发 | 当调用 Effect.forkChild 时,运行时会生成新的 Fiber 来处理并发操作。 |
| 协作式让出 | 它确保 Fiber 不会独占资源,并在必要时让出控制权。 |
| 确保资源清理 | 运行时保证终结器正确运行,以便在需要时清理资源。 |
| 处理异步回调 | 运行时透明地处理异步操作,让你可以用统一的方式编写异步与同步代码。 |
使用显式 context 运行
当我们使用运行 effect 的函数(如 Effect.runPromise 或 Effect.runFork)时,我们完全不需要提及任何运行时对象。并不存在一个单独的“默认运行时”值需要查找或传递。这些函数直接执行 Effect<A, E, never>,使用空的 context 以及默认 services。
如果你的 effect 仍有未满足的需求(R 不是 never),而你又已经拥有满足这些需求的 Context<R>——例如通过 Context.make 手动构建、而非经由 Layer 构建的 context——那么你可以直接用对应的 Effect.run*With 函数运行它,而不必先用 Effect.provide 把它包起来:
示例(使用显式 context 同步运行)
import { Context, Effect } from "effect"
// Define a service and its shape
class MathService extends Context.Service<
MathService,
{ readonly add: (a: number, b: number) => number }
>()("MathService") {}
// Build a context providing an implementation directly
const context = Context.make(MathService, {
add: (a, b) => a + b,
})
const program = Effect.gen(function* () {
const math = yield* MathService
return math.add(2, 3)
})
Effect.runSyncWith(context)(program) // => 5
在大多数场景下,这种直接的方式已足以执行 effect。不过,有些情况下构建一个可复用的 runtime 会很有帮助,尤其是当你需要在许多次独立调用之间复用特定配置或 context 时。
例如,在 React 应用里,或者在服务器上响应 API 请求执行操作时,你可能想用 ManagedRuntime 从一个 layer Layer<R, Err, RIn> 构建可复用的 runtime,下一节会介绍。这样你就能在不同的执行边界之间保持一致的 context。
局部作用域的运行时配置
在 Effect 中,运行时配置通常从父级工作流继承。这意味着,当我们在某个工作流内部访问运行时配置或获取一个 runtime 时,实际上使用的就是父级工作流的配置。
不过,有时我们想临时覆盖代码中某个特定部分的运行时配置。这个概念称为局部作用域的运行时配置。一旦该代码区域的执行结束,运行时配置就会恢复为原来的设置。
为此,我们使用 Effect.provide,它允许我们把新的运行时配置提供给代码的某个特定区段。
示例(覆盖 Logger 配置)
在这个示例中,我们创建一个包含自定义 logger 的 layer,它记录消息时不带时间戳和级别。然后我们用 Effect.provide 把这个 logger layer 应用到程序上。
import { Logger, Effect, Fiber, Exit } from "effect"
const addSimpleLogger = Logger.layer([
// Custom logger implementation
Logger.make(({ message }) => console.log(message)),
])
const program = Effect.gen(function* () {
yield* Effect.log("Application started!")
yield* Effect.log("Application is about to exit!")
})
// Running with the default logger
Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Application started!"
timestamp=... level=INFO fiber=#0 message="Application is about to exit!"
*/
// Overriding the default logger with a custom one
const fiber = Effect.runFork(program.pipe(Effect.provide(addSimpleLogger)))
/*
Output:
[ 'Application started!' ]
[ 'Application is about to exit!' ]
*/
Effect.runSync(Fiber.await(fiber)) // => Exit.succeed(undefined)
为了确保运行时配置只应用于 Effect 应用的某个特定部分,我们应该只把配置 layer 提供给那一个部分。
示例(把配置 layer 提供给嵌套的工作流)
在这个示例中,我们演示如何只把自定义 logger 配置应用到程序的某个特定区段。程序的大部分都使用默认 logger,但当我们应用 Effect.provide(addSimpleLogger) 调用时,它会覆盖那个特定嵌套块内部的 logger。之后,配置会恢复为原来的状态。
import { Logger, Effect } from "effect"
const addSimpleLogger = Logger.layer([
// Custom logger implementation
Logger.make(({ message }) => console.log(message)),
])
const removeDefaultLogger = Logger.layer([])
const program = Effect.gen(function* () {
// Logs with default logger
yield* Effect.log("Application started!")
yield* Effect.gen(function* () {
// This log is suppressed
yield* Effect.log("I'm not going to be logged!")
// Custom logger applied here
yield* Effect.log("I will be logged by the simple logger.").pipe(
Effect.provide(addSimpleLogger),
)
// This log is suppressed
yield* Effect.log(
"Reset back to the previous configuration, so I won't be logged.",
)
}).pipe(
// Remove the default logger temporarily
Effect.provide(removeDefaultLogger),
)
// Logs with default logger again
yield* Effect.log("Application is about to exit!")
})
Effect.runSync(program) // => undefined
/*
Output:
timestamp=... level=INFO fiber=#0 message="Application started!"
[ 'I will be logged by the simple logger.' ]
timestamp=... level=INFO fiber=#0 message="Application is about to exit!"
*/
ManagedRuntime
在开发 Effect 应用并使用 Effect.run* 函数执行它时,应用会在幕后自动使用默认 runtime 运行。虽然可以通过 Effect.provide 提供局部作用域的配置 layer 来调整应用的特定部分,但有些场景下你可能想从顶层为整个应用自定义运行时配置。
在这些情况下,你可以使用 ManagedRuntime.make 构造器把一个配置 layer 转换成 runtime,从而创建顶层 runtime。
示例(创建并使用自定义 ManagedRuntime)
在这个示例中,我们首先创建一个名为 appLayer 的自定义配置 layer,它用一个会把消息输出到控制台的简单 logger 替换默认 logger。接着,我们用 ManagedRuntime.make 把这个配置 layer 变成 runtime。
import { Effect, ManagedRuntime, Logger } from "effect"
// Define a configuration layer that replaces the default logger
const appLayer = Logger.layer([
// Custom logger implementation
Logger.make(({ message }) => console.log(message)),
])
// Create a custom runtime from the configuration layer
const runtime = ManagedRuntime.make(appLayer)
const program = Effect.log("Application started!")
// Execute the program using the custom runtime
runtime.runSync(program) // => undefined
// Clean up resources associated with the custom runtime
Effect.runFork(runtime.disposeEffect)
/*
Output:
[ 'Application started!' ]
*/
Context.Service
在与需要四处传递的 runtime 打交道时,Context.Service 可以简化对 service 的访问。它让你可以把 service key 及其形状一起定义成单个类。
示例(为通知定义一个 service)
import { Context, Effect } from "effect"
class Notifications extends Context.Service<
Notifications,
{ readonly notify: (message: string) => Effect.Effect<void> }
>()("Notifications") {}
Notifications.key // => "Notifications"
使用 .use()(见下文)可以用解析后的 service 运行一个回调,或者在 Effect.gen 中通过 yield* Notifications 直接访问它。
这让你可以直接与该 service 交互:
示例(使用 Notifications service key)
import { Context, Effect, Layer } from "effect"
class Notifications extends Context.Service<
Notifications,
{ readonly notify: (message: string) => Effect.Effect<void> }
>()("Notifications") {}
// Create an effect that depends on the Notifications service
//
// ┌─── Effect<void, never, Notifications>
// ▼
const action = Notifications.use((n) => n.notify("Hello, world!"))
Effect.runSync(
action.pipe(
Effect.provide(Layer.succeed(Notifications, { notify: () => Effect.void })),
),
) // => undefined
在这个示例中,action effect 依赖于 Notifications service。这种方式让你无需手动传递就能引用 service。之后,你可以创建一个提供 Notifications service 的 Layer,并用该 layer 构建 ManagedRuntime,以确保该 service 在需要的地方可用。
集成
ManagedRuntime 简化了 service 与 layer 同其他框架或工具的集成,尤其是在 Effect 并非主要框架、且对主入口点的访问受到限制的环境中。
例如,在 React 这类框架或环境中,你对应用主入口点的控制有限,ManagedRuntime 有助于管理 service 的生命周期。
下面介绍如何在外部框架中管理 service 的生命周期:
示例(在外部框架中使用 ManagedRuntime)
import { Context, Effect, ManagedRuntime, Layer, Console } from "effect"
// Define the Notifications service using Context.Service
class Notifications extends Context.Service<
Notifications,
{ readonly notify: (message: string) => Effect.Effect<void> }
>()("Notifications") {
// Provide a live implementation of the Notifications service
static Live = Layer.succeed(this, {
notify: (message) => Console.log(message),
})
}
// Example entry point for an external framework
async function main() {
// Create a custom runtime using the Notifications layer
const runtime = ManagedRuntime.make(Notifications.Live)
// Run the effect
const result = await runtime.runPromise(
Notifications.use((n) => n.notify("Hello, world!")),
)
// Dispose of the runtime, cleaning up resources
await runtime.dispose()
return result
}
await main() // => undefined