951 words
5 minutes
Effect 类型安全错误处理:5.pipe、双重 API 和 Effect.succeed

我们解决了部分问题。实际的错误处理分为 2 个步骤:

  • 收集可能的错误
  • 处理错误

我们用 tryPromise 完成了”收集”部分,但仍然缺少”处理”部分

实际上,现在运行 main 在出错时仍然会使应用崩溃:

import { Effect } from "effect";

const fetchRequest = Effect.tryPromise(() =>
  fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);

const jsonResponse = (response: Response) =>
  Effect.tryPromise(() => response.json());

const main = Effect.flatMap(fetchRequest, jsonResponse);

/// 👇 当出错时会抛出异常(`UnknownException`)
Effect.runPromise(main);
> effect-getting-started-course@1.0.0 dev
> tsx src/index.ts

node:internal/process/promises:289
            triggerUncaughtException(err, true /* fromPromise */);
            ^

UnknownException: An unknown error occurred
    at e

在运行 Effect 之前,我们编写一些代码来定义当 Effect 包含 UnknownException 时会发生什么。

这个操作称为从错误中恢复

使用 pipe 组合 Effect#

我们将大量使用函数。将函数相互嵌套很快就会变得难以阅读。

让我们想象一下为程序添加另一个步骤:

const fetchRequest = Effect.tryPromise(() =>
  fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);

const jsonResponse = (response: Response) =>
  Effect.tryPromise(() => response.json());

const savePokemon = (pokemon: unknown) =>
  Effect.tryPromise(() =>
    fetch("/api/pokemon", { body: JSON.stringify(pokemon) })
  );

我们如何组合这第三个步骤?另一个 flatMap 看起来是这样的:

const main = Effect.flatMap(
  Effect.flatMap(fetchRequest, jsonResponse),
  savePokemon
);

程序从哪里开始?这些操作按什么顺序执行?再次强调,难以阅读且难以维护

别担心!有一个更好看的解决方案:pipe

import { Effect, pipe } from "effect";

const main = pipe(
  fetchRequest,
  Effect.flatMap(jsonResponse),
  Effect.flatMap(savePokemon)
);

pipe 获取一个函数的结果并将其提供(“管道传输”)给链中的下一个函数。

const main: number = pipe(
  10,
  (num) => num.toString(), // 👈 `num` 是 10
  (str) => str.length > 0, // 👈 `str` 是 `num.toString()`
  (bool) => Number(bool) // 👈 `bool` 是 `str.length > 0`
);

现在你可以将程序读作从上到下执行的一系列步骤

pipe 内的每个参数都是一个函数,它提供前一个函数的结果,如下所示:

const main = pipe(
  fetchRequest,
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse),
  (jsonResponseEffect) => Effect.flatMap(jsonResponseEffect, savePokemon)
);

由于这种模式随处可见,每个 Effect 都带有自己的 pipe 函数。因此我们可以进一步改进代码(无需导入 pipe):

import { Effect } from "effect";

const main = fetchRequest.pipe(
  Effect.flatMap(jsonResponse),
  Effect.flatMap(savePokemon)
);

Effect Playground

注意这与 Promisethen 方法有多么相似:

const main = fetchRequest.then(
  (response) => jsonResponse(response).then(
    (json) => savePokemon(json)
  )
);

Effect 双重 API#

在之前的 pipe 代码中,我们从这样:

const main = fetchRequest.pipe(
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse)
);

变成了这样:

const main = fetchRequest.pipe(
  Effect.flatMap(jsonResponse)
);

这里发生了什么?

在 Effect 中,大多数 API 都是双重的。这意味着它们接受多个或单个参数:

  • 在同一个 flatMap 中传递多个参数
const main = pipe(
  fetchRequest,
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse)
);
  • 逐个传递单个参数
const main = pipe(
  fetchRequest,
  // 👉 注意 `fetchRequestEffect` 和 `jsonResponse` 与之前相比是颠倒的
  (fetchRequestEffect) => Effect.flatMap(jsonResponse)(fetchRequestEffect)
);

这是 flatMap 的(简化)定义:

export declare const flatMap: {
  // 👇 先是函数(`jsonResponse`),然后是 `Effect`
  <A, B>(f: (a: A) => Effect<B>): (self: Effect<A>) => Effect<B>

  // 👇 `Effect` 和函数(`jsonResponse`)在同一个 `flatMap` 中
  <A, B>(self: Effect<A>, f: (a: A) => Effect<B>): Effect<B>
}

一次传递一个参数的技术称为部分应用

在 Effect 中,函数可以是”数据优先”或”数据最后”。

它用于更容易地组合函数:

const dataFirst = (n: number, str: string) => n + str.length;
const dataLast = (str: string) => (n: number) => n + str.length;

[1, 2, 3, 4].map((value) => dataFirst(value, "abc"));

/// 👇 直接传递函数,不需要中间的 `value`
[1, 2, 3, 4].map(dataLast("abc"));

这允许将代码简化为一系列可读的步骤

const main = fetchRequest.pipe(
  (fetchRequestEffect) => Effect.flatMap(jsonResponse)(fetchRequestEffect)
);
const main = fetchRequest.pipe(
  // 👇 函数组合
  Effect.flatMap(jsonResponse)
);

在 Effect 中包装值:succeed#

当我们将任何 API 包装在 Effect 内部时,目标是组合其他 Effect,并且只在最后离开”Effect 世界”并运行程序。

因此,Effect 提供了一些函数来将值包装在 Effect 内部。Effect.succeed 就是这样做的:

const num: number = 10;
const numEffect: Effect<number> = Effect.succeed(10);
Effect 类型安全错误处理:5.pipe、双重 API 和 Effect.succeed
https://0bipinnata0.my/posts/course/effect-beginners-complete-getting-started/type-safe-error-handling-with-effect/pipe-dual-and-effect-succeed/
Author
0bipinnata0
Published at
2025-08-30 13:25:04