Effect’s dependency management can seem like magic. Before we dive into more advanced concepts, let’s peel back a few layers to see how it is implemented.
We can define a service class by extending Context.Tag (16)
, or in a more explicit fashion using Context.GenericTag.
import { Context, Effect } from "effect";
interface RandomService { next: Effect.Effect<number>;}
const RandomTag = Context.GenericTag<RandomService>("@myapp/RandomService");
const fakeImpl = { next: Effect.succeed(1),};
const roll = Effect.gen(function* (_) { const randomImpl = yield* RandomTag; const randomValue = yield* randomImpl.next; return Math.ceil(randomValue * 6);});
roll.pipe( Effect.provideService(RandomTag, fakeImpl), Effect.runSync,);In this case, Context.Tag (16)
is probably easier. Context.GenericTag can be very useful when
- you don’t want to define a whole service class (e.g. when the “service” is a primitive value)
- you want to decouple service definition from Effect
import ThirdPartyService from "third-party";
// defining a class is overkill for a simple stringconst LogLevel = Context.GenericTag<"INFO" | "DEBUG">("@myapp/LogLevel");
// this can be a quick way to get movingconst ThirdParty = Context.GenericTag<ThirdPartyService>("@myapp/ThirdParty");From a tag we can construct a Context.Context. This is a type-safe wrapper on top of a JavaScript Map, where each key (a string) may have values of different types (the service implementation). Note that constructing a context is a synchronous (non-effectful) operation.
const RandomContext = RandomTag.context(fakeImpl);console.log(RandomContext.unsafeMap);Seeing this implementation explains why it is a good practice to use namespaced keys. The key is used as the key of a Map, so poorly named keys could lead to collisions. When using Effect.provideService (17)
(and related functions) the services are added to an implicit “default” Context.
const A1 = Context.GenericTag<number>("A");const A2 = Context.GenericTag<string>("A");
const useA = Effect.gen(function* (_) { const a1 = yield* A1; const a2 = yield* A2; console.log(`a1 = ${a1}, a2 = ${a2}`);});
useA.pipe( Effect.provideService(A2, "not ideal"), Effect.provideService(A1, 42), Effect.runSync,);Context.GenericTag can take a second type parameter, which helps with type hinting when there are multiple distinct services of the same type. This would be particularly common for primitives.
const PrimaryUrl = Context.GenericTag<"primary", string>("@myapp/PrimaryUrl");const ReadReplicaUrl = Context.GenericTag<"secondary", string>( "@myapp/ReadReplicaUrl",);
const useDatabase = Effect.gen(function* (_) { const primary = yield* PrimaryUrl; const replica = yield* ReadReplicaUrl; // these values are distinct, because the _keys_ are distinct console.log(`primary = ${primary}, replica = ${replica}`);});
// the type checker is able to tell we need two distinct services,// even though both services are implemented as stringsuseDatabase.pipe( Effect.provideService( PrimaryUrl, "postgres://user:pw@localhost:5432/primary", ), Effect.provideService( ReadReplicaUrl, "postgres://user:pw@localhost:5432/secondary", ), Effect.runSync,);When using the two parameter Context.GenericTag<Id, Service> format, make sure that the Id is globally unique. Otherwise the type-checker may get confused.
const StatusCode = Context.GenericTag<"status", number>("@myapp/http/Status");const OrderStatus = Context.GenericTag<"status", string>( "@myapp/orders/Status",);
const retrieveOrder = Effect.gen(function* (_) { const fetched = yield* StatusCode; if (fetched >= 400) { return yield* Effect.fail("oh dear"); } const shipping = yield* OrderStatus; console.log(`your order is ${shipping}`);});
// this passes type checking, even though we have not provided an OrderStatus// (it will die at runtime)retrieveOrder.pipe( Effect.provideService(StatusCode, 200), Effect.runSync,);
// note that this is _only_ a type checker issue// the runtime keys/values do not overlap// the following code runs as expectedretrieveOrder.pipe( Effect.provideService(OrderStatus, "out for delivery"), Effect.provideService(StatusCode, 200), Effect.runSync,);One way to ensure that a type is globally unique is to use a unique symbol.
This is a common convention in Effect code, especially older code that predates Context.Tag (16)
.
interface Port { readonly _: unique symbol;}
interface UserId { readonly _: unique symbol;}
const Port = Context.GenericTag<Port, number>("@myapp/Port");const UserId = Context.GenericTag<UserId, number>("@myapp/UserId");
const buildUserUrl = Effect.gen(function* (_) { console.log(`https://localhost:${yield* Port}/users/${yield* UserId}`);});
buildUserUrl.pipe( Effect.provideService(Port, 5000), Effect.provideService(UserId, 7357), Effect.runSync,);Looking at this final form, we can see roughly how Context.Tag (16)
works. There is a bit of magic going on behind the scenes, but it is not much more than a Context.GenericTag. Here is a side-by-side comparison of the two in action. As with errors, being able to define a typical service as a class enables some handy shortcuts.
import { Effect, Context } from "effect";
interface PlainRandom { next: Effect.Effect<number>;}
const randomImpl = { next: Effect.sync(() => Math.random()),};
// ---- using GenericTag for controlinterface RandomServiceG { readonly _: unique symbol;}
const RandomServiceG = Context.GenericTag<RandomServiceG, PlainRandom>( "@myapp/RandomServiceG",);
// ┌─── RandomServiceG// ▼type RandomServiceGId = Context.Tag.Identifier<typeof RandomServiceG>;
// ┌─── PlainRandom// ▼type RandomServiceGService = Context.Tag.Service<typeof RandomServiceG>;
const Live = RandomServiceG.of(randomImpl);
const Test = RandomServiceG.of({ next: Effect.succeed(0.5),});
// ---- using Tag for convenienceexport class RandomServiceT extends Context.Tag("@myapp/RandomServiceT")< RandomServiceT, PlainRandom>() { static Live = this.of(randomImpl);
static Test = this.of({ next: Effect.succeed(0.5), });}
// ┌─── RandomServiceT// ▼type RandomServiceTId = Context.Tag.Identifier<RandomServiceT>;
// ┌─── PlainRandom// ▼type RandomServiceTService = Context.Tag.Service<RandomServiceT>;Let’s recap:
Context.GenericTagallows defining a tag representing a placeholder for a service- You can create a
Context.ContextusingMyTag.context(impl) - a
Contextis a type-safe wrapper on top of aMap - when using tags, it is important to use unique key values and identifier types
Context.Tag(16) is (approximately) syntactic sugar on top ofContext.GenericTag