In the previous lesson we saw how combining layers can adjust the input and output service types. This is not the only way to combine layers. Consider what happens when we apply Layer.merge (33)
to our familiar DiceService and RandomService.
import { Effect, Layer } from "effect";import { DiceService, RandomService } from "./lib";
const layerRandom = Layer.succeed(RandomService, { next: Effect.sync(() => Math.random()),});
const makeDice = Effect.gen(function* () { const random = yield* RandomService; return { roll: random.next.pipe(Effect.map((v) => Math.ceil(v * 6))), };});
const layerDiceNeedsRandom = Layer.effect(DiceService, makeDice);
// ┌─── Layer.Layer<RandomService | DiceService, never, RandomService>// ▼const layerDiceMerged = Layer.merge(layerRandom, layerDiceNeedsRandom);The input types are combined, so this layer will require a RandomService as an input (because layerDiceNeedsRandom requires a RandomService), even though it also produces a RandomService as an output (because layerRandom produces a RandomService).
We can use Layer.provide to specify that layerDiceNeedsRandom should “use” layerRandom.
// ┌─── Layer.Layer<DiceService, never, never>// ▼const layerDiceProvided = Layer.provide(layerDiceNeedsRandom, layerRandom);
const main = Effect.gen(function* () { const dice = yield* DiceService; const rolled = yield* dice.roll; console.log(`rolled ${rolled}`);});
main.pipe(Effect.provide(layerDiceProvided), Effect.runSync);Note that Layer.provide is similar to Effect.provide (26)
. It helps us “fill in” the missing requirements until we have a Layer<Services, Errors, never>.
Note that Layer.provide is asymmetrical, unlike Layer.merge (33)
.
// ┌─── Layer.Layer<RandomService, never, RandomService>// ▼const layerRandomUsingDice = Layer.provide(layerRandom, layerDiceNeedsRandom);This backwards layer does not “fill in” the RandomService dependency, nor does it produce the DiceService. For this reason, I often find it easier to use the piped form of Layer.provide, even when only working with two layers.
Let’s see a more complex example using both Layer.merge (33)
and Layer.provide.
const business = 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,// | MissingEnvironmentVariable | DatabaseConnectionError,// | never// | >// ▼const RootLive = PizzaRepository.Live.pipe( Layer.provide(Database.Live), Layer.merge(Basket.Live), Layer.provide(DiscountCodes.Live));
business.pipe(Effect.provide(RootLive), Effect.runPromise);The order of provides is important, but there are multiple ways to solve this. The type hints will help you figure out whether you have provided dependencies in the correct order.
You can build up your app in terms of stratified tiers, by merging the layers in each tier first, and then combining those tiers. In this case, we build up all of the “Config” tier first, and all of the “Application Services” tier. This works neatly if none of the Config layers use any of the Application Services layers.
const ConfigLive = Layer.merge(Database.Live, DiscountCodes.Live);const AppLive = Layer.merge(Basket.Live, PizzaRepository.Live);const RootLive2 = AppLive.pipe(Layer.provide(ConfigLive));
business.pipe(Effect.provide(RootLive2), Effect.runPromise);Alternatively, you can decompose your app into vertical slices, where you provide all dependencies needed to make a single high-level application service work.
const PizzaRepositoryUsingDatabase = PizzaRepository.Live.pipe( Layer.provide(Database.Live));const BasketUsingDiscountCodes = Basket.Live.pipe( Layer.provide(DiscountCodes.Live));const RootLive3 = Layer.merge( PizzaRepositoryUsingDatabase, BasketUsingDiscountCodes);
business.pipe(Effect.provide(RootLive3), Effect.runPromise);With these two combinators you can stitch together all of your application’s dependencies into one large Layer, which you can then provide to the main program that will run.
Let’s recap:
- There are multiple ways to combine Layers
- Providing a Layer can “fill in” the requirements
- The combined Layer will fail if either Layer fails to build