我们解决了部分问题。实际的错误处理分为 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)
);注意这与 Promise 的 then 方法有多么相似:
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);