已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

错误累积

学习在 Effect 工作流中有效地管理错误:掌握顺序执行、错误累积与结果划分的工具。

诸如 Effect.zipEffect.allEffect.forEach 这类顺序组合子,在错误管理上采用「快速失败」(fail fast)策略。这意味着它们一旦遇到第一个错误,就会立即停止并返回。

下面是一个使用 Effect.zip 的示例,它会在第一个失败处停止,并且只显示第一个错误:

示例(使用 Effect.zip 快速失败)

import { Effect, Console } from "effect"

const task1 = Console.log("task1").pipe(Effect.as(1))
const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2))
const task3 = Console.log("task2").pipe(Effect.as(3))
const task4 = Effect.fail("Oh no!").pipe(Effect.as(4))

const program = task1.pipe(
  Effect.zip(task2),
  Effect.zip(task3),
  Effect.zip(task4),
)

Effect.runPromise(program).then(console.log, console.error)
/*
Output:
task1
(FiberFailure) Error: Oh uh!
*/

Effect.forEach 函数的行为与之类似。它会把一个产生 effect 的操作应用到集合中的每个元素,但遇到第一个错误时就会停止:

示例(使用 Effect.forEach 快速失败)

import { Effect, Console } from "effect"

const program = Effect.forEach([1, 2, 3, 4, 5], (n) => {
  if (n < 4) {
    return Console.log(`item ${n}`).pipe(Effect.as(n))
  } else {
    return Effect.fail(`${n} is not less that 4`)
  }
})

Effect.runPromise(program).then(console.log, console.error)
/*
Output:
item 1
item 2
item 3
(FiberFailure) Error: 4 is not less that 4
*/

不过,有些场景下你可能希望收集所有错误,而不是快速失败。在这些情况下,你可以使用那些同时累积成功与错误的函数。

validate

Effect.validate 函数与 Effect.zip 类似,但即使遇到错误,它也会继续组合各个 effect,从而同时累积成功与失败。

示例(校验并收集错误)

import { Effect, Console } from "effect"

const task1 = Console.log("task1").pipe(Effect.as(1))
const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2))
const task3 = Console.log("task2").pipe(Effect.as(3))
const task4 = Effect.fail("Oh no!").pipe(Effect.as(4))

const program = task1.pipe(
  Effect.validate(task2),
  Effect.validate(task3),
  Effect.validate(task4),
)

Effect.runPromiseExit(program).then(console.log)
/*
Output:
task1
task2
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Sequential',
    left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },
    right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }
  }
}
*/

validateAll

Effect.validateAll 函数与 Effect.forEach 函数类似。它使用所提供的、会产生 effect 的操作来转换集合中的所有元素,但它会把所有错误收集到错误通道中,同时把成功值收集到成功通道中。

import { Effect, Console } from "effect"

//      ┌─── Effect<number[], string[], never>
//      ▼
const program = Effect.validateAll([1, 2, 3, 4, 5], (n) => {
  if (n < 4) {
    return Console.log(`item ${n}`).pipe(Effect.as(n))
  } else {
    return Effect.fail(`${n} is not less that 4`)
  }
})

Effect.runPromiseExit(program).then(console.log)
/*
Output:
item 1
item 2
item 3
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: [ '4 is not less that 4', '5 is not less that 4' ]
  }
}
*/
Loss of Successes

注意,这个函数是有损的:只要存在任何错误,所有成功值都会丢失。如果你需要同时保留成功与失败, 可以考虑使用 Effect.partition

validateFirst

Effect.validateFirst 函数与 Effect.validateAll 类似,但它会返回第一个成功的结果;如果没有任何元素成功,则返回所有错误。

示例(返回第一个成功结果)

import { Effect, Console } from "effect"

//      ┌─── Effect<number, string[], never>
//      ▼
const program = Effect.validateFirst([1, 2, 3, 4, 5], (n) => {
  if (n < 4) {
    return Effect.fail(`${n} is not less that 4`)
  } else {
    return Console.log(`item ${n}`).pipe(Effect.as(n))
  }
})

Effect.runPromise(program).then(console.log, console.error)
/*
Output:
item 4
4
*/

注意,Effect.validateFirst 返回的成功类型是单个 number,而不像 Effect.validateAll 那样返回结果数组。

partition

Effect.partition 函数会处理一个可迭代对象,并把一个会产生 effect 的函数应用到每个元素上。它返回一个元组,其中第一部分包含所有失败,第二部分包含所有成功。

示例(划分成功与失败)

import { Effect } from "effect"

//      ┌─── Effect<[string[], number[]], never, never>
//      ▼
const program = Effect.partition([0, 1, 2, 3, 4], (n) => {
  if (n % 2 === 0) {
    return Effect.succeed(n)
  } else {
    return Effect.fail(`${n} is not even`)
  }
})

Effect.runPromise(program).then(console.log, console.error)
/*
Output:
[ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ]
*/

这个操作符是一个不会失败的 effect(unexceptional effect),也就是说其错误通道类型为 never。失败会被收集而不会中断该 effect,因此整个操作会执行完成,并同时返回错误与成功。