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

Layer 的记忆化

学习 Layer 的记忆化如何通过复用 Layer 并控制其实例化来优化 Effect 应用中的性能。

Layer 的记忆化允许一个 Layer 只创建一次,并在依赖图中被多次使用。如果我们两次使用同一个 Layer:

Layer.merge(Layer.provide(L2, L1), Layer.provide(L3, L1))

那么 L1 这个 Layer 只会被分配一次。

Avoid Duplicate Layer Creation

Layer 的记忆化基于引用相等性。因此,如果你有一个通过调用 f() 这类函数创建的 Layer,就应当 调用一次 f,并复用得到的 Layer, 以确保你始终在使用同一个实例。

全局提供时的记忆化

Effect 应用的一个重要特性是:Layer 默认会被共享。这意味着,如果同一个 Layer 被使用了两次,并且我们以全局方式提供它,那么这个 Layer 只会被分配一次。对于依赖图中的每个 Layer,都只有一个实例,在所有依赖它的 Layer 之间共享。

示例

例如,假设我们有三个 Service:ABCBC 的实现都依赖 A 这个 Service:

import { Effect, Context, Layer } from "effect"

class A extends Context.Service<A, { readonly a: number }>()("A") {}

class B extends Context.Service<B, { readonly b: string }>()("B") {}

class C extends Context.Service<C, { readonly c: boolean }>()("C") {}

let initCount = 0

const ALive = Layer.effect(
  A,
  Effect.succeed({ a: 5 }).pipe(
    Effect.tap(() => Effect.log("initialized")),
    Effect.tap(() => Effect.sync(() => initCount++)),
  ),
)

const BLive = Layer.effect(
  B,
  Effect.gen(function* () {
    const { a } = yield* A
    return { b: String(a) }
  }),
)

const CLive = Layer.effect(
  C,
  Effect.gen(function* () {
    const { a } = yield* A
    return { c: a > 0 }
  }),
)

const program = Effect.gen(function* () {
  yield* B
  yield* C
})

const runnable = Effect.provide(
  program,
  Layer.merge(Layer.provide(BLive, ALive), Layer.provide(CLive, ALive)),
)

await Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
*/

// ALive is only built once, no matter how many layers depend on it
initCount // => 1

尽管 BLiveCLive 这两个 Layer 都需要 ALive,但 ALive 只会被实例化一次。它被 BLiveCLive 共享。

获取全新版本

如果我们不想共享某个模块,就应该通过 Layer.fresh 创建一个全新的、不共享的版本。

示例

import { Effect, Context, Layer } from "effect"

class A extends Context.Service<A, { readonly a: number }>()("A") {}

class B extends Context.Service<B, { readonly b: string }>()("B") {}

class C extends Context.Service<C, { readonly c: boolean }>()("C") {}

let initCount = 0

const ALive = Layer.effect(
  A,
  Effect.succeed({ a: 5 }).pipe(
    Effect.tap(() => Effect.log("initialized")),
    Effect.tap(() => Effect.sync(() => initCount++)),
  ),
)

const BLive = Layer.effect(
  B,
  Effect.gen(function* () {
    const { a } = yield* A
    return { b: String(a) }
  }),
)

const CLive = Layer.effect(
  C,
  Effect.gen(function* () {
    const { a } = yield* A
    return { c: a > 0 }
  }),
)

const program = Effect.gen(function* () {
  yield* B
  yield* C
})

const runnable = Effect.provide(
  program,
  Layer.merge(
    Layer.provide(BLive, Layer.fresh(ALive)),
    Layer.provide(CLive, Layer.fresh(ALive)),
  ),
)

await Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
timestamp=... level=INFO fiber=#3 message=initialized
*/

// Layer.fresh forces a brand-new (non-shared) instance for each usage
initCount // => 2

局部提供时不进行记忆化

如果我们不以全局方式提供 Layer,而是在局部提供它们,那么该 Layer 默认不支持记忆化。

示例

在下面的例子中,我们在局部两次提供了 ALive Layer,Effect 不会对 ALive 的构造进行记忆化。 因此,它会被初始化两次:

import { Effect, Context, Layer } from "effect"

class A extends Context.Service<A, { readonly a: number }>()("A") {}

let initCount = 0

const ALive = Layer.effect(
  A,
  Effect.succeed({ a: 5 }).pipe(
    Effect.tap(() => Effect.log("initialized")),
    Effect.tap(() => Effect.sync(() => initCount++)),
  ),
)

const program = Effect.gen(function* () {
  yield* Effect.provide(A, ALive)
  yield* Effect.provide(A, ALive)
})

await Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=initialized
timestamp=... level=INFO fiber=#0 message=initialized
*/

// Providing the layer locally rebuilds it every time, so it runs twice
initCount // => 2

手动记忆化

我们可以使用 Layer.MemoMap 手动对一个 Layer 进行记忆化。MemoMap 会记录哪些 Layer 已经构建过,这样再次构建同一个 Layer 时,只要针对同一个 MemoMap,就会复用之前的结果,而不是重新执行它的获取 Effect。

使用 Layer.makeMemoMap 创建一个 MemoMap,然后用 Layer.buildWithMemoMap 把一个 Layer 构建到 Context 中,同时传入 MemoMap 和一个 Scope

示例

import { Effect, Context, Layer } from "effect"

class A extends Context.Service<A, { readonly a: number }>()("A") {}

let initCount = 0

const ALive = Layer.effect(
  A,
  Effect.succeed({ a: 5 }).pipe(
    Effect.tap(() => Effect.log("initialized")),
    Effect.tap(() => Effect.sync(() => initCount++)),
  ),
)

const program = Effect.gen(function* () {
  const memoMap = yield* Layer.makeMemoMap
  const scope = yield* Effect.scope

  const context1 = yield* Layer.buildWithMemoMap(ALive, memoMap, scope)
  const context2 = yield* Layer.buildWithMemoMap(ALive, memoMap, scope)

  yield* Effect.provide(A, context1)
  yield* Effect.provide(A, context2)
})

await Effect.runPromise(Effect.scoped(program))
/*
Output:
timestamp=... level=INFO fiber=#1 message=initialized
*/

// Building ALive twice against the same MemoMap reuses the first result
initCount // => 1