简化过度嵌套
使用 Do 模拟与 generator 简化嵌套代码。
假设你想创建一个自定义函数 elapsed,用来打印某个 effect 执行所耗费的时间。
使用普通的 pipe
最初,你可能会写出使用标准 pipe 方法的代码,但这种方式会导致过度嵌套,让代码变得冗长且难以阅读:
示例(使用 pipe 测量耗时)
import { Effect, Console } from "effect"
// Get the current timestamp
const now = Effect.sync(() => new Date().getTime())
// Prints the elapsed time occurred to `self` to execute
const elapsed = <R, E, A>(
self: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
now.pipe(
Effect.andThen((startMillis) =>
self.pipe(
Effect.andThen((result) =>
now.pipe(
Effect.andThen((endMillis) => {
// Calculate the elapsed time in milliseconds
const elapsed = endMillis - startMillis
// Log the elapsed time
return Console.log(`Elapsed: ${elapsed}`).pipe(
Effect.map(() => result),
)
}),
),
),
),
),
)
// Simulates a successful computation with a delay of 200 milliseconds
const task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)
/*
Output:
Elapsed: 204
some task
*/
为了解决这个问题并让代码更易于维护,有一个方案:「do 模拟」(do simulation)。
使用「do 模拟」
Effect 中的「do 模拟」让你能以更声明式的风格编写代码,类似于其他编程语言中的「do notation」。它提供了一种定义变量、并通过 Effect.bind、Effect.let 这类函数对它们执行操作的方式。
do 模拟的工作方式如下:
-
使用
Effect.Do值启动 do 模拟:const program = Effect.Do.pipe(/* ... rest of the code */) -
在 do 模拟的作用域内,你可以使用
Effect.bind函数定义变量,并把它绑定到Effect值:Effect.bind("variableName", (scope) => effectValue)variableName是你为要定义的变量选择的名字。它在作用域内必须唯一。effectValue是你想绑定到该变量的Effect值。它可以是函数调用的结果,也可以是任何其他合法的Effect值。
-
你可以累积多个
Effect.bind语句,在作用域内定义多个变量:Effect.bind("variable1", () => effectValue1), Effect.bind("variable2", ({ variable1 }) => effectValue2), // ... additional bind statements -
在 do 模拟作用域内,你还可以使用
Effect.let函数定义变量,并把它绑定到简单值:Effect.let("variableName", (scope) => simpleValue)variableName是你给变量起的名字。和之前一样,它在作用域内必须唯一。simpleValue是你想赋给该变量的值。它可以是number、string或boolean这样的简单值。
-
在 do 模拟中仍然可以使用
Effect.andThen、Effect.flatMap、Effect.tap和Effect.map这类常规 Effect 函数。在作用域内,这些函数会把累积的变量作为参数接收:Effect.andThen(({ variable1, variable2 }) => { // Perform operations using variable1 and variable2 // Return an `Effect` value as the result })
借助 do 模拟,你可以像这样重写 elapsed 函数:
示例(使用 do 模拟测量耗时)
import { Effect, Console } from "effect"
// Get the current timestamp
const now = Effect.sync(() => new Date().getTime())
const elapsed = <R, E, A>(
self: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
Effect.Do.pipe(
Effect.bind("startMillis", () => now),
Effect.bind("result", () => self),
Effect.bind("endMillis", () => now),
Effect.let(
"elapsed",
// Calculate the elapsed time in milliseconds
({ startMillis, endMillis }) => endMillis - startMillis,
),
// Log the elapsed time
Effect.tap(({ elapsed }) => Console.log(`Elapsed: ${elapsed}`)),
Effect.map(({ result }) => result),
)
// Simulates a successful computation with a delay of 200 milliseconds
const task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)
/*
Output:
Elapsed: 204
some task
*/
使用 Effect.gen
最简洁、最方便的解决方案是使用 Effect.gen,它让你在处理 effect 时可以使用 generator。这种方式利用了 generator 语法提供的原生作用域,避免了过度嵌套,从而让代码更简洁。
示例(使用 Effect.gen 测量耗时)
import { Effect } from "effect"
// Get the current timestamp
const now = Effect.sync(() => new Date().getTime())
// Prints the elapsed time occurred to `self` to execute
const elapsed = <R, E, A>(
self: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
Effect.gen(function* () {
const startMillis = yield* now
const result = yield* self
const endMillis = yield* now
// Calculate the elapsed time in milliseconds
const elapsed = endMillis - startMillis
// Log the elapsed time
console.log(`Elapsed: ${elapsed}`)
return result
})
// Simulates a successful computation with a delay of 200 milliseconds
const task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)
/*
Output:
Elapsed: 204
some task
*/
在 generator 内部,我们使用 yield* 调用 effect,并把它们的结果绑定到变量。这消除了嵌套,提供了更易读、更顺序化的代码结构。
Effect 中的 generator 风格采用更加线性、顺序化的执行流程,类似于传统的命令式编程语言。这让代码更易读、更易理解,尤其是对更熟悉命令式编程范式的开发者而言。