Layer.succeed

Over the last few lessons we have seen how to build up Contexts and provide them to Effects. We’ve seen that we can manipulate Contexts without running any Effects. Layers combine aspects of Context and Effect together, helping us to build up apps that have complex chains of dependencies.

The simplest way to make a Layer is with Layer.succeed. Recall that Effect.succeed (1) allowed us to wrap a plain value as an Effect. Similarly Layer.succeed allows us to wrap a plain value as a Layer.

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

export class RandomService extends Context.Tag("@fred.c/RandomService")<
  RandomService,
  { next: Effect.Effect<number> }
>() {}

const nextRandom = Effect.sync(() => Math.random());

//        ┌─── Layer.Layer<RandomService, never, never>
//        ▼
const randomLayer = Layer.succeed(RandomService, { next: nextRandom });
console.dir(randomLayer, { depth: undefined });

Looking at the type signature, we can see that a Layer has three type parameters. These are similar to the type parameters of an Effect. The first type parameter (RandomService, in this case) represents the “output”. This Layer knows how to produce a RandomService when asked. We will see examples in later lessons that show how the “error” and “requirements” types behave.

Looking at the output, we see that a Layer is a plain data structure. Indeed, it looks fairly similar to an Effect, in terms of having an _op and various instructions. We can also see an unsafeMap in there, which seems familiar from working with Context. As with Effects, the internals are hidden away from the type system, so we will mostly ignore them.

$ npx tsx main.ts
{
  _op_layer: 'FromEffect',
  effect: EffectPrimitiveSuccess {
    _op: 'Success',
    effect_instruction_i0: {
      unsafeMap: Map(1) {
        '@fred.c/RandomService' => {
          next: EffectPrimitive {
            _op: 'Sync',
            effect_instruction_i0: [Function (anonymous)],
            effect_instruction_i1: undefined,
            effect_instruction_i2: undefined,
            trace: undefined,
            [Symbol(effect/Effect)]: {
              _R: [Function: _R],
              _E: [Function: _E],
              _A: [Function: _A],
              _V: '3.13.10'
            }
          }
        }
      }
    },
    effect_instruction_i1: undefined,
    effect_instruction_i2: undefined,
    trace: undefined,
    _tag: 'Success',
    [Symbol(effect/Effect)]: {
      _R: [Function: _R],
      _E: [Function: _E],
      _A: [Function: _A],
      _V: '3.13.10'
    }
  }
}

Having built a layer, we can then provide it to an Effect.

//     ┌─── Effect.Effect<void, never, RandomService>
//     ▼
const main = RandomService.pipe(
  Effect.flatMap((random) => random.next),
  Effect.map((value) => console.log(`random value: ${value}`))
);

main.pipe(Effect.provide(randomLayer), Effect.runSync);

For good measure, let’s see one more example, using a different service.

// dice.ts
import { Context, Effect, Layer } from "effect";

class DiceService extends Context.Tag("@fred.c/DiceService")<
  DiceService,
  { roll: Effect.Effect<number> }
>() {}

const nextRandom = Effect.sync(() => Math.random());

const realDice = {
  roll: nextRandom.pipe(Effect.map((v) => Math.ceil(v * 6))),
};

//        ┌─── Layer.Layer<DiceService, never, never>
//        ▼
const diceLayer = Layer.succeed(DiceService, realDice);

const main = DiceService.pipe(
  Effect.flatMap((dice) => dice.roll),
  Effect.map((score) => console.log(`you rolled ${score}`))
);

main.pipe(Effect.provide(diceLayer), Effect.runSync);

To construct a Layer<DiceService> we need to pass the DiceService tag definition itself along with an implementation of Context.Tag.Service<DiceService>. Or, more generally, to constructor a Layer<I>, we need to provide a Tag<I, S> and an S.

const succeed: <I, S>(tag: Context.Tag<I, S>, resource: Types.NoInfer<S>): Layer<I>

So far we have not gained much, because all of the above separation was possible using Context. In the next lesson we will see the key benefit of introducing the Layer data type: constructing the service instance can itself be effectful.

Let’s recap:

  • Layer is another data type and module in the Effect library
  • Layer combines aspects of Effect and Context
  • You can provide a Layer to an Effect using Effect.provide(layer)
  • Effect.provide (25) works with Layers or Contexts
  • a Layer conceptually represents a constructor