Context.GenericTag
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 string
const LogLevel = Context.GenericTag<"INFO" | "DEBUG">("@myapp/LogLevel");
// this can be a quick way to get moving
const 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 strings
useDatabase.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 expected
retrieveOrder.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 control
interface 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 convenience
export 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.GenericTag
allows defining a tag representing a placeholder for a service- You can create a
Context.Context
usingMyTag.context(impl)
- a
Context
is 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