Layer.merge
There are multiple ways to combine Layers. Layer.merge
behaves like a “union” operation. The merged layer provides all the services from both input layers, but it also requires all the dependencies from both input layers.
We have seen something similar before. Recall that Context.merge
(24)
behaves like a union operation on two Contexts, creating a larger Context containing the union of all the tags.
Here is a simple example, using the same services from Layer.succeedContext
(28)
. We can see that the output services are merged. In this sense the resulting layer is “larger” or “wider” than the arguments.
import { Context, Effect, Layer } from "effect";
import { DiceService, CoinService, business } from "./lib";
const DiceTest = Layer.succeed(DiceService, {
roll: Effect.succeed(3),
});
const CoinTest = Layer.succeed(CoinService, {
flip: Effect.succeed("HEADS" as const),
});
// ┌─── Layer.Layer<DiceService | CoinService, never, never>
// ▼
const MergedTest = Layer.merge(DiceTest, CoinTest);
business.pipe(Effect.provide(MergedTest), Effect.runSync);
Let’s also see how this affects the input services. For the sake of brevity the full definition of these services are not included. In this scenario PizzaLive
requires Database
and BasketLive
requires DiscountCodes
. This means that the merged layer, AppLive
, requires both Database | DiscountCodes
.
// ┌─── Layer.Layer<PizzaRepository, never, Database>
// ▼
const PizzaLive = Layer.effect(PizzaRepository, makePizzaRepository);
// ┌─── Layer.Layer<Basket, never, DiscountCodes>
// ▼
const BasketLive = Layer.effect(Basket, makeBasket);
// ┌─── Effect.Effect<void, never, PizzaRepository | Basket>
// ▼
const program = Effect.gen(function* () {
const allPizzas = yield* PizzaRepository.all;
yield* Basket.addToBasket(allPizzas[0]);
yield* Basket.addToBasket(allPizzas[1]);
yield* Basket.addToBasket(allPizzas[1]);
yield* Basket.applyDiscountCode("EFFECT15");
console.log(`your pizzas will cost ${yield* Basket.total}`);
});
// ┌─── Layer.Layer<
// | PizzaRepository | Basket,
// | never,
// | Database | DiscountCodes
// | >
// ▼
const AppLive = Layer.merge(PizzaLive, BasketLive);
program.pipe(
Effect.provide(AppLive),
Effect.provideServiceEffect(Database, makeDatabase),
Effect.provideServiceEffect(DiscountCodes, makeDiscountCodes),
Effect.runPromise
);
The error types also get merged.
// ┌─── Layer.Layer<DiscountCodes, MissingEnvironmentVariable, never>
// ▼
const DiscountCodesLive = Layer.effect(DiscountCodes, makeDiscountCodes);
// ┌─── Layer.Layer<Database, MissingEnvironmentVariable | DatabaseConnectionError, never>
// ▼
const DatabaseLive = Layer.effect(Database, makeDatabase);
// ┌─── Layer.Layer<
// | Database | DiscountCodes,
// | MissingEnvironmentVariable | DatabaseConnectionError,
// | never
// | >
// ▼
const ConfigLive = Layer.merge(DiscountCodesLive, DatabaseLive);
program.pipe(
Effect.provide(AppLive),
Effect.provide(ConfigLive),
Effect.runPromise
);
Let’s look at the type signature. Although there are many generic type parameters, the signature is not too complex because of the symmetry. Note that the merging happens in the same way in each of the output, error and input channels. Notice the symmetry between the two arguments (Layer.merge(Foo, Bar)
will return a Layer of the same type as Layer.merge(Bar, Foo)
). As always there is a dual implementation to support self.pipe(Layer.merge(that))
, but this is not shown.
type Merge = <RIn, E1, ROut, RIn2, E2, ROut2>(
self: Layer.Layer<ROut, E1, RIn>,
that: Layer.Layer<ROut2, E2, RIn2>
) => Layer.Layer<ROut | ROut2, E1 | E2, RIn | RIn2>;
We can implement Layer.merge
using Layer.map
(31)
, Layer.flatMap
and Context.merge
(24)
. (The real implementation is more optimised.)
const merge: Merge = (self, that) =>
Layer.flatMap(self, (c1) => Layer.map(that, (c2) => Context.merge(c1, c2)));
Let’s recap:
- There are multiple ways to combine Layers
- Merging Layers produces a “wider” layer
- Merging Layers will merge each channel separately