Layer.succeedContext

We have seen two ways of constructing a Layer so far. Layer.succeed (27) allows us to construct a Layer from a value. Layer.effect (28) allows us to construct a Layer from an Effect. Layer.succeedContext (29) allows us to construct a Layer from a Context.

In particular, this allows a Layer to provide multiple services. Consider this code that requires multiple services.

import { Context, Data, Effect } from "effect";
export class DiceService extends Context.Tag("@fred.c/DiceService")<
DiceService,
{ roll: Effect.Effect<number> }
>() {}
export type Coin = "HEADS" | "TAILS";
export class CoinService extends Context.Tag("@fred.c/CoinService")<
CoinService,
{ flip: Effect.Effect<Coin> }
>() {}
export class YouLose extends Data.TaggedError("@fred.c/YouLose") {}
export const business = Effect.gen(function* () {
const dice = yield* DiceService;
const coin = yield* CoinService;
let total = yield* dice.roll;
const flip = yield* coin.flip;
if (flip === "HEADS") {
total += yield* dice.roll;
}
if (total < 5) {
yield* new YouLose();
}
return total;
});

We can define implementations for each of these services in a dedicated module, exporting only the layer.

import { Context, Effect, Layer } from "effect";
import { DiceService, CoinService } from "./lib";
const nextRandom = Effect.sync(() => Math.random());
const makeDice = DiceService.of({
roll: nextRandom.pipe(Effect.map((v) => Math.ceil(v * 6))),
});
const makeCoin = CoinService.of({
flip: nextRandom.pipe(Effect.map((v) => (v > 0.5 ? "HEADS" : "TAILS"))),
});
const context = Context.merge(
DiceService.context(makeDice),
CoinService.context(makeCoin)
);
// ┌─── Layer.Layer<DiceService | CoinService>
// ▼
export const Live = Layer.succeedContext(context);

Then we can provide a single layer that satisfies all the dependencies.

import { Effect } from "effect";
import { business } from "./lib";
import { Live } from "./layer";
business.pipe(
Effect.map(console.log),
Effect.catchAll((e) => Effect.succeed(console.error(e))),
Effect.provide(Live),
Effect.runSync
);

Note that this is very similar to how we worked with a Context before in Effect.provide (26) . The key difference is that a Context can only be constructed in a pure (non-effectful) way. A Layer can be constructed in an effectful or a non-effectful way. The type signature will be the same either way.

Observe that we can implement Effect.provide(context) in terms of Effect.provide(layer).

import { Effect, Layer, Context } from "effect";
export const provide = <A, E, R1, R2>(
self: Effect.Effect<A, E, R1>,
context: Context.Context<R2>
): Effect.Effect<A, E, Exclude<R1, R2>> => {
const layer = Layer.succeedContext(context);
return Effect.provide(self, layer);
};

Let’s recap:

  • Layer combines aspects of Effect and Context
  • You can provide a Layer to an Effect using Effect.provide(layer)
  • a Layer conceptually represents a constructor
  • a Layer can be constructed in an effectul or a non-effectful way