Layer 的记忆化
学习 Layer 的记忆化如何通过复用 Layer 并控制其实例化来优化 Effect 应用中的性能。
Layer 的记忆化允许一个 Layer 只创建一次,并在依赖图中被多次使用。如果我们两次使用同一个 Layer:
Layer.merge(Layer.provide(L2, L1), Layer.provide(L3, L1))
那么 L1 这个 Layer 只会被分配一次。
Layer 的记忆化基于引用相等性。因此,如果你有一个通过调用 f()
这类函数创建的 Layer,就应当 只 调用一次 f,并复用得到的 Layer,
以确保你始终在使用同一个实例。
全局提供时的记忆化
Effect 应用的一个重要特性是:Layer 默认会被共享。这意味着,如果同一个 Layer 被使用了两次,并且我们以全局方式提供它,那么这个 Layer 只会被分配一次。对于依赖图中的每个 Layer,都只有一个实例,在所有依赖它的 Layer 之间共享。
示例
例如,假设我们有三个 Service:A、B 和 C。B 和 C 的实现都依赖 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
尽管 BLive 和 CLive 这两个 Layer 都需要 ALive,但 ALive 只会被实例化一次。它被 BLive 和 CLive 共享。
获取全新版本
如果我们不想共享某个模块,就应该通过 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