我们如何将代码库从 fp-ts 迁移到 Effect | 作者:Laure Retru-Chavastel | inato | 2024 年 6 月

[*][*]

[*]

[*]

[*]

先天

在 Inato,我们于 2024 年初从 fp-ts 迁移到 Effect。鉴于我们庞大的代码库(约 50 万行 TypeScript 代码),我们需要一种方法来确保任何新代码都可以使用 Effect 编写,同时允许现有的 fp-ts 代码共存。我们仅用两个月就实现了这一目标,为此投入了大约 10% 的时间。在本文中,您将找到我们详细的迁移策略、我们开发的帮助程序(您可以在此找到 存储库),以及我们如何确保代码库的顺利过渡。

本文的 Dev.to 版本已发布 这里

在 Inato,我们很早就非常积极地采用函数式编程,因此我们从 2020 年初开始在我们的代码库中使用 fp-ts。如果您想了解更多信息,请查看我们的函数式编程之旅。

现在让我们进入正题:今年年初,我们正式决定改用 Effect!为什么?

  • fp-ts 的主要维护者(gcanti👋) 加入 Effect 团队 这可能意味着 fp-ts 方面的开发不太活跃,而 Effect 显然是下一步的开发目标。
  • 由于 fp-ts 的学习曲线较高,且缺乏文档。过去几年加入 Inato 的开发人员经常提到这一点:学习 fp-ts 并不是一件容易的事情。而这正是 Effect 的强项,它拥有一流的文档和大量的培训资源。
  • 欲了解更多理由,请访问 比较 fp-ts 和 Effect 的 Effect 网站

将我们的代码库迁移到 Effect 是一个伟大的目标,但事实证明,这样做更具挑战性,需要仔细规划。我们还想限制花在这个项目上的时间,所以我们同意了 2.5 个月的期限。考虑到所有这些,我们制定了以下策略。

首先,这是我们服务器端代码库的表示:我们有代表业务行为的用例,这些用例具有多个依赖项(服务、存储库等 – 我们将它们称为端口),我们还有将执行我们的用例的运行器:

当我们开始迁移时,我们有大约 400 个用例和 80 个端口及其适配器需要迁移。

我们这次迁移的目标很明确:在 2.5 个月的窗口期结束时,任何新的用例或端口都将使用 Effect 编写。为了实现平稳过渡,让 fp-ts 和 Effect 代码能够共存,我们制定了以下计划:

  1. 确保我们的港口恢复 ReaderTaskEither 促进向 Effect 的过渡 [*]
  2. 创建我们端口的效果代理:fp-ts 中只有一个实现,但可以使用 fp-ts“真实”版本和每个端口的效果代理版本
  3. 开始在 Effect 中(重新)编写用例
  4. 创建 Effect 用例的 fp-ts 代理
  5. 开始在 Effect 中(重新)编写端口
  6. 创建 Effect 端口的 fp-ts 代理:此时,我们已经实现了使用 Effect 编写新用例和端口的目标。但我们想再接再厉,覆盖整个流程!
  7. 能够运行 Effect 和 fp-ts 用例

[*] ReaderTaskEither (我们将其称为 RTE (后来)是促进向 Effect 迁移的先决条件。为什么?从概念上讲, ReaderTaskEither 可以表示如下:

ReaderTaskEither
= Reader>
= (context: R) => () => Promise>

如果我们看一下对效果的表示 官方网站,我们可以看到这些是非常相似的概念(这是我们在迁移过程中将利用的概念):

Effect ~ (context: Context) => E | A

现在让我们深入研究代码!以下是我们将要遵循的步骤:

为了说明我们的迁移过程,我们将重点关注一个代表我们的代码库如何组织的示例程序。

注意:所有代码和帮助程序都可以在👉中找到 这个存储库 👈

迁移计划

假设我们的领域模型由一个简单的 Foo 班级:

// domain.ts
export class Foo {
constructor(readonly id: string) {}
static make = (id = "random-id") => new Foo(id);
}

我们定义一个存储库端口来获取和存储 Foo

// FooRepository.ts
export interface FooRepository {
getById: (id: string) => RTE;
store: (foo: Foo) => RTE;
}

export interface FooRepositoryAccess {
fooRepository: FooRepository;
}
export declare const FooRepository: {
getById: (id: string) => RTE;
store: (foo: Foo) => RTE;
};
export declare const makeFooRepository: () => Promise;

笔记:

  • 我们遵循 模块模式 在定义 FooRepositoryAccess 在组合多个时启用上下文聚合的接口 ReaderTaskEither
declare const a: RTE<{ serviceA: ServiceA },never,void>
declare const b: RTE<{ serviceB: ServiceB },never,void>
const ab: RTE<{ serviceA: ServiceA; serviceB: ServiceB },never,void>
= rte.flatMap(a,() => b)
  • 我们定义一个 伴随对象 FooRepository 它公开与存储库本身相同的方法,只是它们每个都需要一个上下文 FooRepositoryAccess. 这使得以后的代码更加简洁:
const theLongWay: RTE = pipe(
rte.ask(),
rte.flatMap(({ fooRepository }) => fooRepository.getById('id'))
);

const theEasyWay: RTE
= FooRepository.getById('id')

我们还定义了一个服务端口来转换 Foo

// TransformFooService.ts
export interface TransformFooService {
transform: (foo: Foo) => RTE;
}

export interface TransformFooServiceAccess {
transformFooService: TransformFooService;
}

export declare const TransformFooService: {
transform: (foo: Foo) => RTE;
};

declare const makeTransformFooService: () => Promise;

接下来我们可以编写两个用例:一个用于创建新的 Foo ,另一个用来转换 Foo

// usecases.ts
export const createFooUseCase = (id:string) =>
pipe(
rte.of(Foo.make(id)),
rte.tap(FooRepository.store)
);

export const transformFooUseCase = (id: string) =>
pipe(
FooRepository.getById(id),
rte.flatMap(TransformFooService.transform),
rte.flatMap(FooRepository.store)
);

最后,我们可以编写 main 这将创建所有端口适配器并调用我们的用例:

// index.ts
const main = async () => {
const fooRepository = await makeFooRepository();
const transformFooService = await makeTransformFooService();
await createFooUseCase("my-foo-id")({
transformFooService,
fooRepository,
})();
await transformFooUseCase("my-foo-id")({
transformFooService,
fooRepository,
})();
};
main();

创建端口的效果代理

此步骤包括生成新的伴随对象 FooRepositoryTransformFooService 对于我们的端口,我们公开了成员方法的效果版本。

首先我们重命名伴生对象,添加一个 Fpts 后缀:

// FooRepository.ts
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶{̶
export declare const FooRepositoryFpts: {
getById: (id: string) => RTE;
store: (foo: Foo) => RTE;
};

// TransformFooService.ts
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶{̶
export declare const TransformFooServiceFpts: {
transform: (foo: Foo) => RTE;
};

// usecases.ts
export const createFooUseCase = (id:string) =>
pipe(
rte.of(Foo.make(id)),
r̶t̶e̶.̶t̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶s̶t̶o̶r̶e̶)̶
rte.tap(FooRepositoryFpts.store)
);

export const transformFooUseCase = (id: string) =>
pipe(
F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶g̶e̶t̶B̶y̶I̶d̶(̶i̶d̶)̶,̶
r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶.̶t̶r̶a̶n̶s̶f̶o̶r̶m̶)̶,̶
r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶s̶t̶o̶r̶e̶)̶
FooRepositoryFpts.getById(id),
rte.flatMap(TransformFooServiceFpts.transform),
rte.flatMap(FooRepositoryFpts.store)
);

然后我们使用 portToEffect 辅助函数从先前的伴随对象生成 Effect 伴随对象:

// FooRepository.ts
export const FooRepositoryTag = Context.GenericTag(
"FooRepository"
);
export const FooRepository = portToEffect(FooRepositoryFpts, {
fooRepository: FooRepositoryTag,
}); // { getById: (id: string) => Effect ... }

// TransformFooService.ts
export const TransformFooServiceTag = Context.GenericTag(
"TransformFooService"
);
export const TransformFooService = portToEffect(TransformFooServiceFpts, {
transformFooService: TransformFooServiceTag,
}); // { transform: (foo: Foo) => Effect }

在 Effect 中重写用例

此时我们可以开始使用新生成的 Effect 伴随对象来重写 transformFooUseCase 用例效果。请注意,我们自愿留下 createFooUseCase 按原样模拟用例的迁移,而不是“大爆炸”迁移,我们会将所有用例一次性转换为 Effect(更加困难和冒险)。

// usecases.ts
export const transformFooUseCase = (id: string) =>
pipe(
F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶.̶g̶e̶t̶B̶y̶I̶d̶(̶i̶d̶)̶,̶
r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶.̶t̶r̶a̶n̶s̶f̶o̶r̶m̶)̶,̶
r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶.̶s̶t̶o̶r̶e̶)̶
FooRepository.getById(id),
Effect.flatMap(TransformFooService.transform),
Effect.flatMap(FooRepository.store)
); // Effect

因为我们不想影响我们的 main 程序尚未开发,我们必须维护此用例的 fp-ts 版本,以实现向后兼容性。我们可以从 Effect 版本生成它,这要归功于 functionToFpts 辅助功能:

// usecases.ts
export const transformFooUseCaseFpts = functionToFpts(transformFooUseCase, {
fooRepository: FooRepositoryTag,
transformFooService: TransformFooServiceTag,
}); // RTE

// index.ts
const main = async () => {
const fooRepository = await makeFooRepository();
const transformFooService = await makeTransformFooService();
await createFooUseCase("my-foo-id")({
transformFooService,
fooRepository,
})();
a̶w̶a̶i̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶U̶s̶e̶C̶a̶s̶e̶(̶"̶m̶y̶-̶f̶o̶o̶-̶i̶d̶"̶)̶(̶{̶
await transformFooUseCaseFpts("my-foo-id")({
transformFooService,
fooRepository,
})();
};
main();

将端口转换为效果

接下来我们将转换 FooRepository 直接将端口移植到 Effect 中:

// FooRepository.ts
export interface FooRepository {
g̶e̶t̶B̶y̶I̶d̶:̶ ̶(̶i̶d̶:̶ ̶s̶t̶r̶i̶n̶g̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶
s̶t̶o̶r̶e̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶v̶o̶i̶d̶>̶;̶
getById: (id: string) => Effect.Effect;
store: (foo: Foo) => Effect.Effect;
}

我们现在可以使用以下方式生成 Effect 伴随对象 Effect.serviceFunctions

// FooRepository.ts
e̶x̶p̶o̶r̶t̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶ ̶=̶ ̶p̶o̶r̶t̶T̶o̶E̶f̶f̶e̶c̶t̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶,̶ ̶{̶
̶ ̶ ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶T̶a̶g̶,̶
̶}̶)̶;̶
export const FooRepository = Effect.serviceFunctions(FooRepositoryTag);

最后,为了向后兼容,我们必须维护 fp-ts 伴随对象。我们可以使用 portToFpts 辅助功能:

// FooRepository.ts
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶:̶ ̶{̶
̶ ̶ ̶g̶e̶t̶B̶y̶I̶d̶:̶ ̶(̶i̶d̶:̶ ̶s̶t̶r̶i̶n̶g̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶
̶ ̶ ̶s̶t̶o̶r̶e̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶v̶o̶i̶d̶>̶;̶
̶}̶;̶

export const FooRepositoryFpts = portToFpts(FooRepository, {
fooRepository: FooRepositoryTag,
}); // { getById: (id: string) => RTE; ... }

我们对 TransformFooService 港口:

// TransformFooService.ts
export interface TransformFooService {
t̶r̶a̶n̶s̶f̶o̶r̶m̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶
transform: (foo: Foo) => Effect;
}

e̶x̶p̶o̶r̶t̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶ ̶=̶ ̶p̶o̶r̶t̶T̶o̶E̶f̶f̶e̶c̶t̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶,̶ ̶{̶
̶ ̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶T̶a̶g̶,̶
̶}̶)̶;̶
export const TransformFooService = Effect.serviceFunctions(
TransformFooServiceTag
);

e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶:̶ ̶{̶
̶ ̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶
̶}̶;̶
export const TransformFooServiceFpts = portToFpts(TransformFooService, {
fooRepository: FooRepositoryTag,
}); // { transform: (foo: Foo) => RTE; }

请注意,我们没有改变我们的 main 在此步骤中它仍然可以无问题地运行。

使用 ManagedRuntime 运行 Effect 用例

为了运行 transformFooUseCase 作为一种效果,我们必须能够通过层提供我们的端口:

// FooRepository.ts
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶m̶a̶k̶e̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶(̶)̶ ̶=̶>̶ ̶P̶r̶o̶m̶i̶s̶e̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶>̶;̶
export declare const FooRepositoryLive: Layer.Layer;

// TransformFooService.ts
d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶m̶a̶k̶e̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶(̶)̶ ̶=̶>̶ ̶P̶r̶o̶m̶i̶s̶e̶<̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶>̶;̶
declare const TransformFooServiceLive: Layer.Layer;

接下来我们可以创建一个 ManagedRuntime 并使用 contextToFpts 帮手:

// index.ts
const main = async () => {
c̶o̶n̶s̶t̶ ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶ ̶=̶ ̶a̶w̶a̶i̶t̶ ̶m̶a̶k̶e̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶(̶)̶;̶
c̶o̶n̶s̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶ ̶=̶ ̶a̶w̶a̶i̶t̶ ̶m̶a̶k̶e̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶(̶)̶;̶
const runtime = ManagedRuntime.make(
Layer.mergeAll(FooRepositoryLive, TransformFooServiceLive)
);
const { context } = await runtime.runtime();
const { fooRepository, transformFooService } = contextToFpts(context, {
fooRepository: FooRepositoryTag,
transformFooService: TransformFooServiceTag,
});
await createFooUseCase("my-foo-id")({
transformFooService,
fooRepository,
})();
await transformFooUseCaseFpts("my-foo-id")({
transformFooService,
fooRepository,
})();
};
main();

最后,我们可以使用运行时来运行 Effect transformFooUseCase

// index.ts
const main = async () => {
const runtime = ManagedRuntime.make(
Layer.mergeAll(FooRepositoryLive, TransformFooServiceLive)
);
const { context } = await runtime.runtime();
const { fooRepository, transformFooService } = contextToFpts(context, {
fooRepository: FooRepositoryTag,
transformFooService: TransformFooServiceTag,
});
await createFooUseCase("my-foo-id")({
transformFooService,
fooRepository,
})();
a̶w̶a̶i̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶U̶s̶e̶C̶a̶s̶e̶F̶p̶t̶s̶(̶"̶m̶y̶-̶f̶o̶o̶-̶i̶d̶"̶)̶(̶{̶
̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶,̶
̶ ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶,̶
}̶)̶(̶)̶;̶
await runtime.runPromise(transformFooUseCase("my-foo-id"));
};
main();

请注意,我们再次离开了 createFooUseCase 按原样迁移用例以表明我们可以处于混合状态,其中只有部分用例已迁移到 Effect。

奖励:简化 fp-ts ↔ 效果标签映射管理

我们在整个迁移过程中使用的所有帮助程序都需要一个映射对象,以便从 fp-ts 端口访问接口的键名(例如 transformFooServiceTransformFooServiceAccess)到 Tag 相应效果端口的设置。例如:

contextToFpts(context, {
fooRepository: FooRepositoryTag,
transformFooService: TransformFooServiceTag,
});

此映射对于所有帮助程序的正常工作至关重要。总是这样制作它们并不理想。为了帮助我们做到这一点,我们引入了:

const FptsConvertibleId = Symbol();
interface FptsConvertible {
[FptsConvertibleId]: T;
}

我们现在可以将此转换信息嵌入到我们的端口的类型级别:

// FooRepository.ts
export interface FooRepository extends FptsConvertible<"fooRepository"> {
getById: (id: string) => Effect.Effect;
store: (foo: Foo) => Effect.Effect;
}

// TransformFooService.ts
export interface TransformFooService
extends FptsConvertible<"transformFooService"> {
transform: (foo: Foo) => Effect;
}

我们能做的第一件事是使用类型助手来简化访问接口的定义 FptsAccess

// FooRepository.ts
export interface FooRepositoryAccess extends FptsAccess {}

// TransformFooService.ts
export interface TransformFooServiceAccess
extends FptsAccess {}

我们还可以使用新的辅助函数定义更小的原子映射对象 getFptsMapping

// FooRepository.ts
const FooRepositoryFptsMapping = getFptsMapping(
FooRepositoryTag,
"fooRepository"
); // { fooRepository: FooRepositoryTag }

// TransformFooService.ts
const TransformFooServiceFptsMapping = getFptsMapping(
TransformFooServiceTag,
"transformFooService"
); // { transformFooService: TransformFooServiceTag }

注意:看起来我们又一次输入了密钥 "fooRepository" 或者 "transformFooService" 但实际上,该函数 getFptsMapping 是类型安全的,因此 FooRepositoryTag 作为第一个参数,只有字符串 "fooRepository" 作为第二个参数有效。因此您的代码编辑器将为您自动完成。此外,如果您更改 FptsConvertible 所以这实际上并不是一个额外的负担。

现在我们可以在调用时组合这两个映射对象 contextToFpts 或任何其他帮助者:

contextToFpts(context, {
f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶T̶a̶g̶,̶
t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶T̶a̶g̶,̶
...FooRepositoryFptsMapping,
...TransformFooServiceFptsMapping,
});

我们的目标是能够使用 Effect 编写任何新用例或端口,并在 2 个月内实现(大约花费了 10% 的时间)!

团队合作无疑是这一成功的重要因素:首先,我们必须提到 Stephane Ledorze,因为他独自迁移了我们所有的存储库,并就如何定义迁移策略给了我们很好的建议。我们在 Inato 每周三下午举行的专门“技术会议”上与整个团队一起处理了其余工作:在这些会议期间,我们停止提供功能,以便能够专注于纯技术主题,这是一个很好的机会来迁移我们必须处理的众多端口并让团队加入 Effect。

在我们撰写本文时,我们有大约 150 个完整的 Effect 用例。其余现有用例将在我们需要更新时随时迁移!

我们已经看到了巨大的改进:例如,使用 Effect 仅用几行代码即可实现速率限制,而使用 fp-ts 则需要大量代码才能实现。既然我们已经正式迁移到 Effect 生态系统,我们渴望更多地利用它!

我们希望本文能够激励您从 fp-ts 转向 Effect,如果您有任何问题或意见,请随时发表评论!

Leave a Reply

Your email address will not be published. Required fields are marked *

近期新闻​

编辑精选​