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:
Layercombines aspects ofEffectandContext- 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