Context.make

Context.empty, Context.add and Context.get are enough to do all the main operations on a context. There are many other helper functions. Most of them rely on these three builders.

Context.make creates a context with one item. That item might be using a Context.GenericTag

import { Context } from "effect";

const MinimumScore = Context.GenericTag<"MinimumScore", number>(
	"@fred.c/MinimumScore",
);

const scoreContext = Context.make(MinimumScore, 3);

// ...later
const three = Context.get(scoreContext, MinimumScore);

Or it might be using a Context.Tag

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

const diceContext = Context.make(DiceService, {
    roll: () => Math.ceil(Math.random() * 6),
});

// ...later
const diceRoll = Context.get(diceContext, DiceService).roll();

Note that we could implement this as

const make = <T extends Context.Tag<any, any>>(
	tag: T,
	service: Context.Tag.Service<T>,
): Context.Context<Context.Tag.Identifier<T>> =>
	Context.empty().pipe(Context.add(tag, service));

// usage:
const scoreContext = make(MinimumScore, 2);

Behind the scenes this is implemented by constructing a Map directly.

Here is the implementation:

const ContextProto: Omit<C.Context<unknown>, "unsafeMap"> = {
    // defines a few methods, e.g. `pipe` and `toString`,
    // that should be available on every Context
    // elided here for brevity
}

const makeContext = <Services>(unsafeMap: Map<string, any>): C.Context<Services> => {
  const context = Object.create(ContextProto)
  context.unsafeMap = unsafeMap
  return context
}

const make = <T extends C.Tag<any, any>>(
  tag: T,
  service: C.Tag.Service<T>
): C.Context<C.Tag.Identifier<T>> => makeContext(new Map([[tag.key, service]]))

The Effect source code can be intimidating at first. Most functions are quite short and simple, but it can be hard to unpick how the pieces fit together.

Making a Context with one service is so common that it is exposed directly as a method on tags. Effect does not expose many instance methods, but in this case it is convenient.

const sixContext = MinimumScore.context(6)
const lowRollContext = DiceService.context({
	roll: () => 1
})

Behind the scenes this called Context.make.

Let’s recap

  • A Context maps tags to service implementations
  • Operations on Context are plain (non-effectful) operations
  • There are multiple equivalent ways to make a Context with one service
  • Use whichever is most convenient for your current situation
  • Effect optimises some operations behind the scenes, using internal implementation details