Dependency Memoization

On this page

Layer memoization allows a layer to be created once and used multiple times in the dependency graph. If we use the same layer twice, for example, Layer.merge(Layer.provide(b, a), Layer.provide(c, a)), then the a layer will be allocated only once.

Memoization When Providing Globally

One important feature of an Effect application is that layers are shared by default. This means that if the same layer is used twice, and if we provide the layer globally, the layer will only be allocated a single time. For every layer in our dependency graph, there is only one instance of it that is shared between all the layers that depend on it.

For example, assume we have the three services A, B, and C. The implementation of both B and C is dependent on the A service:

ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
class B extends Context.Tag("B")<B, { readonly b: string }>() {}
 
class C extends Context.Tag("C")<C, { readonly c: boolean }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const b = Layer.effect(
B,
Effect.gen(function* (_) {
const { a } = yield* _(A)
return { b: String(a) }
})
)
 
const c = 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(b, a), Layer.provide(c, a))
)
 
Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
*/
ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
class B extends Context.Tag("B")<B, { readonly b: string }>() {}
 
class C extends Context.Tag("C")<C, { readonly c: boolean }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const b = Layer.effect(
B,
Effect.gen(function* (_) {
const { a } = yield* _(A)
return { b: String(a) }
})
)
 
const c = 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(b, a), Layer.provide(c, a))
)
 
Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
*/

Although both b and c layers require the a layer, the a layer is instantiated only once. It is shared with both b and c.

Acquiring a Fresh Version

If we don't want to share a module, we should create a fresh, non-shared version of it through Layer.fresh.

ts
const runnable = Effect.provide(
program,
Layer.merge(
Layer.provide(b, Layer.fresh(a)),
Layer.provide(c, Layer.fresh(a))
)
)
 
Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
timestamp=... level=INFO fiber=#3 message=initialized
*/
ts
const runnable = Effect.provide(
program,
Layer.merge(
Layer.provide(b, Layer.fresh(a)),
Layer.provide(c, Layer.fresh(a))
)
)
 
Effect.runPromise(runnable)
/*
Output:
timestamp=... level=INFO fiber=#2 message=initialized
timestamp=... level=INFO fiber=#3 message=initialized
*/

No Memoization When Providing Locally

If we don't provide a layer globally but instead provide them locally, that layer doesn't support memoization by default.

In the following example, we provided the a layer two times locally, and Effect doesn't memoize the construction of the a layer. So, it will be initialized two times:

ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const program = Effect.gen(function* (_) {
yield* _(A, Effect.provide(a))
yield* _(A, Effect.provide(a))
})
 
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=initialized
timestamp=... level=INFO fiber=#0 message=initialized
*/
ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const program = Effect.gen(function* (_) {
yield* _(A, Effect.provide(a))
yield* _(A, Effect.provide(a))
})
 
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=initialized
timestamp=... level=INFO fiber=#0 message=initialized
*/

Manual Memoization

We can memoize the a layer manually using the Layer.memoize operator. It will return a scoped effect that, if evaluated, will return the lazily computed result of this layer:

ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const program = Effect.scoped(
Layer.memoize(a).pipe(
Effect.flatMap((memoized) =>
Effect.gen(function* (_) {
yield* _(A, Effect.provide(memoized))
yield* _(A, Effect.provide(memoized))
})
)
)
)
 
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=initialized
*/
ts
import { Effect, Context, Layer } from "effect"
 
class A extends Context.Tag("A")<A, { readonly a: number }>() {}
 
const a = Layer.effect(
A,
Effect.succeed({ a: 5 }).pipe(Effect.tap(() => Effect.log("initialized")))
)
 
const program = Effect.scoped(
Layer.memoize(a).pipe(
Effect.flatMap((memoized) =>
Effect.gen(function* (_) {
yield* _(A, Effect.provide(memoized))
yield* _(A, Effect.provide(memoized))
})
)
)
)
 
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=initialized
*/