使用 Generator
学习如何在 Effect 中使用 Generator 编写带副作用的代码,改善控制流、处理错误,并借助类似 async/await 的语法简化异步操作。
Effect 提供了一种便捷的语法,它类似于 async/await,让你可以使用 generators 编写带副作用的代码。
在 Effect 中,使用 generator 是一个可选特性。如果你觉得 generator 不太熟悉,或者更喜欢其他的编码风格,可以阅读 Effect 中关于构建 管道的文档。
理解 Effect.gen
Effect.gen 工具借助 JavaScript 的 generator 函数,简化了编写带副作用代码的工作。这种方式能让你的代码在外观和行为上更接近传统的同步代码,从而提升可读性并改善错误管理。
示例(执行带折扣的交易)
让我们来看一个实用的程序,它执行一系列在应用逻辑中常见的转换操作:
import { Effect } from "effect"
// Function to add a small service charge to a transaction amount
const addServiceCharge = (amount: number) => amount + 1
// Function to apply a discount safely to a transaction amount
const applyDiscount = (
total: number,
discountRate: number,
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
// Simulated asynchronous task to fetch a transaction amount from a
// database
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
// Simulated asynchronous task to fetch a discount rate from a
// configuration file
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
// Assembling the program using a generator function
const program = Effect.gen(function* () {
// Retrieve the transaction amount
const transactionAmount = yield* fetchTransactionAmount
// Retrieve the discount rate
const discountRate = yield* fetchDiscountRate
// Calculate discounted amount
const discountedAmount = yield* applyDiscount(transactionAmount, discountRate)
// Apply service charge
const finalAmount = addServiceCharge(discountedAmount)
// Return the total amount after applying the charge
return `Final amount to charge: ${finalAmount}`
})
// Execute the program and log the result
Effect.runPromise(program).then(console.log)
// Output: Final amount to charge: 96
使用 Effect.gen 时需要遵循的关键步骤:
- 把逻辑包裹在
Effect.gen中 - 使用
yield*处理 effect - 返回最终结果
如果你在 generator 中通过 yield* 处理的任何一个 effect 失败了,那么 generator 会停止执行,并以该失败退出。
只有在 tsconfig.json 文件中使用 downlevelIteration 标志,或者把
target 设为 "es2015" 或更高版本时,才能使用 generator API。
比较 Effect.gen 与 async/await
如果你熟悉 async/await,可能会注意到两者的代码编写流程很相似。
让我们比较一下这两种方式:
import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number,
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(transactionAmount, discountRate)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (total: number, discountRate: number): Promise<number> =>
discountRate === 0
? Promise.reject(new Error("Discount rate cannot be zero"))
: Promise.resolve(total - (total * discountRate) / 100)
const fetchTransactionAmount = Promise.resolve(100)
const fetchDiscountRate = Promise.resolve(5)
export const program = async function () {
const transactionAmount = await fetchTransactionAmount
const discountRate = await fetchDiscountRate
const discountedAmount = await applyDiscount(transactionAmount, discountRate)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
}需要注意的是,尽管代码看起来相似,但这两个程序并不完全相同。把它们并排比较,只是为了突出它们在写法上的相似之处。
拥抱控制流
在配合 generator 使用 Effect.gen 时,一个显著优势是它能够在 generator 函数内部使用标准的控制流结构。这些结构包括 if/else、for、while 以及其他分支和循环机制,从而增强你在代码中表达复杂控制流逻辑的能力。
示例(使用控制流)
import { Effect } from "effect"
const calculateTax = (
amount: number,
taxRate: number,
): Effect.Effect<number, Error> =>
taxRate > 0
? Effect.succeed((amount * taxRate) / 100)
: Effect.fail(new Error("Invalid tax rate"))
const program = Effect.gen(function* () {
let i = 1
while (true) {
if (i === 10) {
break // Break the loop when counter reaches 10
} else {
if (i % 2 === 0) {
// Calculate tax for even numbers
console.log(yield* calculateTax(100, i))
}
i++
continue
}
}
})
Effect.runPromise(program)
/*
Output:
2
4
6
8
*/
如何抛出错误
Effect.gen API 让你可以通过 yield 一个失败的 effect,把错误处理直接整合进工作流中。
你可以像下面这个示例一样,用 Effect.fail 引入错误。
示例(向流程中引入错误)
import { Effect, Console } from "effect"
const task1 = Console.log("task1...")
const task2 = Console.log("task2...")
const program = Effect.gen(function* () {
// Perform some tasks
yield* task1
yield* task2
// Introduce an error
yield* Effect.fail("Something went wrong!")
})
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
task1...
task2...
(FiberFailure) Error: Something went wrong!
*/
短路的作用
在使用 Effect.gen 时,理解它如何处理错误很重要。
这个 API 会在遇到第一个错误时停止执行,并返回该错误。
这对你的代码有什么影响?如果你有一系列顺序执行的操作,那么其中任何一个失败后,其余操作都不会运行,并且该错误会被返回。
简单来说,如果某个环节出了问题,程序会立刻停在那里,并把错误交给你。
如果你不想在出错时停止,可以使用 Effect.either 方法把错误封装进 Either 数据类型:请参阅管理预期错误的示例。
示例(在第一个错误处停止执行)
import { Effect, Console } from "effect"
const task1 = Console.log("task1...")
const task2 = Console.log("task2...")
const failure = Effect.fail("Something went wrong!")
const task4 = Console.log("task4...")
const program = Effect.gen(function* () {
yield* task1
yield* task2
// The program stops here due to the error
yield* failure
// The following lines never run
yield* task4
return "some result"
})
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
task1...
task2...
(FiberFailure) Error: Something went wrong!
*/
尽管执行永远不会到达失败之后的代码,但除非你在失败后显式 return,否则 TypeScript 仍可能认为错误下方的代码是可到达的。
例如,考虑下面这个场景,你希望收窄某个变量的类型:
示例(没有显式 return 时的类型收窄)
import { Effect } from "effect"
type User = {
readonly name: string
}
// Imagine this function checks a database or an external service
declare function getUserById(id: string): Effect.Effect<User | undefined>
function greetUser(id: string) {
return Effect.gen(function* () {
const user = yield* getUserById(id)
if (user === undefined) {
// Even though we fail here, TypeScript still thinks
// 'user' might be undefined later
yield* Effect.fail(`User with id ${id} not found`)
}
// @errors: 18048
return `Hello, ${user.name}!`
})
}
在这个示例中,TypeScript 仍然认为 user 可能是 undefined,因为失败之后没有显式 return。
要解决这个问题,请在调用 Effect.fail 之后立即显式 return:
示例(有显式 return 时的类型收窄)
import { Effect } from "effect"
type User = {
readonly name: string
}
declare function getUserById(id: string): Effect.Effect<User | undefined>
function greetUser(id: string) {
return Effect.gen(function* () {
const user = yield* getUserById(id)
if (user === undefined) {
// Explicitly return after failing
return yield* Effect.fail(`User with id ${id} not found`)
}
// Now TypeScript knows that 'user' is not undefined
return `Hello, ${user.name}!`
})
}
如果想进一步了解 Effect 中的错误处理,请参阅错误 管理章节。
传递 this
在某些情况下,你可能需要把当前对象(this)的引用传入 generator 函数体。
你可以借助一个把该引用作为第一个参数接收的重载来实现:
示例(向 Generator 传递 this)
import { Effect } from "effect"
class MyClass {
readonly local = 1
compute = Effect.gen(this, function* () {
const n = this.local + 1
yield* Effect.log(`Computed value: ${n}`)
return n
})
}
Effect.runPromise(new MyClass().compute).then(console.log)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Computed value: 2"
2
*/
适配器 Deprecated
你可能仍会遇到一些使用适配器的代码片段,它们通常以 _ 或 $ 符号来标识。
在更早的 TypeScript 版本中,generator「适配器」函数是必需的,用来确保 generator 内部能进行正确的类型推断。这个适配器用于促进 TypeScript 类型系统与 generator 函数之间的交互。
示例(旧代码中的适配器)
import { Effect } from "effect"
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
// Older usage with an adapter for proper type inference
const programWithAdapter = Effect.gen(function* ($) {
const transactionAmount = yield* $(fetchTransactionAmount)
})
// Current usage without an adapter
const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
})
随着 TypeScript(v5.5+)的进步,适配器对于类型推断来说已不再必要。虽然为了向后兼容它仍保留在代码库中,但预计会在 Effect 的下一个主要版本中移除。