Context, Tag и GenericTag
Глубокое погружение в систему контекста Effect.
Теория
Проблема управления зависимостями
В традиционном программировании управление зависимостями превращается в серьёзную проблему по мере роста приложения. Рассмотрим типичный подход:
// ❌ Традиционный подход: передача зависимостей через параметры
const processOrder = (
order: Order,
database: DatabaseService,
logger: LoggerService,
emailService: EmailService,
paymentGateway: PaymentGateway
): Promise<OrderResult> => {
// Реализация
}
const validateUser = (
userId: string,
database: DatabaseService,
logger: LoggerService,
cache: CacheService
): Promise<User> => {
// Реализация
}
Этот подход создаёт несколько проблем:
| Проблема | Описание |
|---|---|
| Prop Drilling | Передача зависимостей через множество уровней вызовов |
| Сложность рефакторинга | Добавление новой зависимости требует изменения сигнатур многих функций |
| Тестирование | Необходимость мокать все зависимости вручную |
| Отсутствие инверсии контроля | Функции явно зависят от конкретных реализаций |
Решение Effect: Context и Tag
Effect предлагает элегантное решение через систему типов, где зависимости декларируются в типе Effect<A, E, R> через параметр R (Requirements):
// ✅ Effect подход: зависимости в типе
// ┌─────── Requirements: зависимости
// ▼
type Program = Effect.Effect<OrderResult, OrderError, Database | Logger | Email>
📊 Архитектура системы Context:
┌─────────────────────────────────────────────────────────────┐
│ Context │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Map<Tag, Service> │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Tag: "DB" │ │ Tag: "Log" │ │ Tag: "HTTP" │ │ │
│ │ │ ─────────── │ │ ─────────── │ │ ─────────── │ │ │
│ │ │ Service: │ │ Service: │ │ Service: │ │ │
│ │ │ Database │ │ Logger │ │ HttpClient │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Что такое Context?
Context — это immutable коллекция, концептуально представляющая собой Map где:
- Ключи — это уникальные теги (
Tag) - Значения — это реализации сервисов
// Концептуальное представление
type Context = ReadonlyMap<Tag<Service>, Service>
💡 Ключевые свойства Context:
- Иммутабельность — Context нельзя изменить, можно только создать новый
- Type-safety — TypeScript гарантирует соответствие тегов и сервисов
- Composability — Context можно объединять через функцию
add - Runtime lookup — O(1) поиск сервиса по тегу
Что такое Tag?
Tag — это уникальный идентификатор сервиса в системе Effect. Tag выполняет две критические функции:
- Идентификация на уровне типов — TypeScript знает, какой тип сервиса связан с тегом
- Идентификация в runtime — Effect может найти нужный сервис в Context
// Создание тега через класс (рекомендуемый способ)
class Database extends Context.Tag("Database")<
Database, // Идентификатор тега (тип класса)
{
// Интерфейс сервиса
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}
>() {}
📖 Анатомия определения Tag:
class Database extends Context.Tag("Database")<
│ │
│ └── Строковый идентификатор (для runtime)
│
└── Имя класса (тип для TypeScript)
Database, // Первый параметр типа: идентификатор тега
{ // Второй параметр типа: shape сервиса
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}
>() {}
Почему нужны строковые идентификаторы?
Строковый идентификатор в Context.Tag("DatabaseService") служит нескольким целям:
// ⚠️ Проблема без идентификатора: горячая перезагрузка
// При hot reload создаётся новый класс, и старый контекст становится несовместимым
// ✅ С идентификатором: стабильность между перезагрузками
class Database extends Context.Tag("app/Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}
💡 Best practices для идентификаторов:
| Паттерн | Пример | Когда использовать |
|---|---|---|
| Namespace prefix | "app/Database" | Приложения |
| Package prefix | "@myorg/auth/Session" | Библиотеки |
| Feature prefix | "orders/Repository" | Feature modules |
GenericTag: низкоуровневый API
GenericTag — это более низкоуровневый API для создания тегов без использования классов:
// Определение интерфейса сервиса отдельно
interface LoggerService {
readonly log: (message: string) => Effect.Effect<void>
readonly error: (message: string) => Effect.Effect<void>
}
// Создание тега через GenericTag
const Logger = Context.GenericTag<LoggerService>("Logger")
// │ │
// │ └── Строковый идентификатор
// └── Тип сервиса
📊 Сравнение Context.Tag vs GenericTag:
| Аспект | Context.Tag (класс) | GenericTag |
|---|---|---|
| Синтаксис | class X extends Context.Tag(...) | Context.GenericTag<T>(...) |
| Nominal typing | ✅ Да | ❌ Нет |
| Instanceof | ✅ Да | ❌ Нет |
| Рекомендация | Production код | Быстрые прототипы |
Context как коллекция
Effect предоставляет богатый API для работы с Context как с коллекцией:
// Определение сервисов
class Config extends Context.Tag("Config")<
Config,
{ readonly apiUrl: string; readonly timeout: number }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
// Создание пустого контекста
const empty = Context.empty()
// Добавление сервисов
const withConfig = Context.add(empty, Config, {
apiUrl: "https://api.example.com",
timeout: 5000
})
// Добавление ещё одного сервиса
const fullContext = Context.add(withConfig, Logger, {
log: (msg) => Effect.sync(() => console.log(msg))
})
// Альтернативный синтаксис через pipe
const contextViaPipe = Context.empty().pipe(
Context.add(Config, { apiUrl: "https://api.example.com", timeout: 5000 }),
Context.add(Logger, { log: (msg) => Effect.sync(() => console.log(msg)) })
)
Концепция ФП: Reader Monad и Dependency Injection
Reader Monad в теории
В функциональном программировании Reader Monad (или Environment Monad) — это паттерн для передачи конфигурации или зависимостей через вычисления без явной передачи параметров.
// Reader<R, A> — вычисление, которое требует R для получения A
type Reader<R, A> = (env: R) => A
// Пример без Effect
const getApiUrl: Reader<Config, string> = (config) => config.apiUrl
const getTimeout: Reader<Config, number> = (config) => config.timeout
const buildUrl: Reader<Config, string> = (config) =>
`${getApiUrl(config)}/data?timeout=${getTimeout(config)}`
Effect как расширенный Reader
Effect расширяет концепцию Reader множеством возможностей:
// Effect<A, E, R> — это по сути Reader<R, Either<E, A>> с добавлением:
// - Ленивости (lazy evaluation)
// - Конкурентности (fibers)
// - Ресурсов (finalizers)
// - Прерываемости (interruption)
class Config extends Context.Tag("Config")<
Config,
{ readonly apiUrl: string }
>() {}
// Effect требует Config для выполнения
const program: Effect.Effect<string, never, Config> = Effect.gen(function* () {
const config = yield* Config
return `URL: ${config.apiUrl}`
})
📊 Эволюция от Reader к Effect:
┌──────────────────────────────────────────────────────────────┐
│ Reader<R, A> │
│ (env: R) => A │
│ │ │
│ ▼ │
│ ReaderEither<R, E, A> │
│ (env: R) => Either<E, A> │
│ │ │
│ ▼ │
│ ReaderTaskEither<R, E, A> │
│ (env: R) => Promise<Either<E, A>> │
│ │ │
│ ▼ │
│ Effect<A, E, R> │
│ + Lazy + Fibers + Resources + Interruption │
└──────────────────────────────────────────────────────────────┘
Inversion of Control
Система Context реализует принцип Inversion of Control (IoC):
// ❌ Прямая зависимость (tight coupling)
const fetchUsers = () => {
const db = new PostgresDatabase() // Жёсткая привязка
return db.query("SELECT * FROM users")
}
// ✅ Инверсия контроля через Effect
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}
const fetchUsers = Effect.gen(function* () {
const db = yield* Database // Зависимость будет предоставлена извне
return yield* db.query("SELECT * FROM users")
})
// Теперь вызывающий код контролирует, какую реализацию использовать
const production = fetchUsers.pipe(
Effect.provideService(Database, postgresImplementation)
)
const test = fetchUsers.pipe(
Effect.provideService(Database, inMemoryImplementation)
)
Algebraic Effects
Концептуально Effect использует идею Algebraic Effects — эффекты как операции, которые можно интерпретировать по-разному:
// "Эффект" — это описание операции, а не её выполнение
const logMessage = (msg: string): Effect.Effect<void, never, Logger> =>
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log(msg)
})
// Один и тот же эффект, разные интерпретации:
// Интерпретация 1: Вывод в консоль
const consoleLogger = { log: (msg: string) => Effect.sync(() => console.log(msg)) }
// Интерпретация 2: Запись в файл
const fileLogger = { log: (msg: string) => Effect.promise(() => writeFile("log.txt", msg)) }
// Интерпретация 3: Отправка на сервер
const remoteLogger = { log: (msg: string) => Effect.promise(() => fetch("/log", { body: msg })) }
API Reference
Context.Tag [STABLE]
Создаёт именованный тег для идентификации сервиса через класс.
declare const Tag: <const Id extends string>(id: Id) => <Self, Shape>() => TagClass<Self, Id, Shape>
// Использование
class MyService extends Context.Tag("MyService")<
MyService, // Идентификатор (обычно имя класса)
ServiceInterface // Тип сервиса
>() {}
Параметры типа:
Id— уникальный идентификатор тега на уровне типовShape— интерфейс предоставляемого сервиса
Context.GenericTag [STABLE]
Создаёт тег без использования класса.
declare const GenericTag: <Identifier, Service = Identifier>(key: string) => Tag<Identifier, Service>
// Использование
const MyService = Context.GenericTag<MyServiceInterface>("MyService")
// ^? Context.Tag<MyServiceInterface, MyServiceInterface>
Context.Reference [STABLE]
Создаёт тег с предоставлением дефолтного значения
declare const Reference: <Self>() => <const Id extends string, Service>(
id: Id,
options: { readonly defaultValue: () => Service }
) => ReferenceClass<Self, Id, Service>
class SpecialNumber extends Context.Reference<SpecialNumber>()("SpecialNumber", {
defaultValue: () => 2048,
}) {}
class Logger extends Context.Reference<Logger>()("Logger", {
defaultValue: () => ({
log: (msg: string) => Effect.sync(() => console.log(msg)),
}),
}) {}
Сервис сразу предоставляется эффекту, но если нужно его изменить можно предоставить другое значение контекста
const program: Effect.Effect<void, never, never> = Effect.gen(function* () {
const specialNumber = yield* SpecialNumber
const logger = yield* Logger
yield* logger.log(`The special number is ${specialNumber}`)
}).pipe(Effect.provideService(SpecialNumber, 1024))
Context.empty [STABLE]
Создаёт пустой контекст.
declare const empty: () => Context<never>
// Использование
const context = Context.empty()
// ^? Context.Context<never>
Context.make [STABLE]
Создаёт контекст с одним сервисом.
declare const make: <T extends Tag<any, any>>(
tag: T,
service: Tag.Service<T>
) => Context<Tag.Identifier<T>>
const context = Context.make(Logger, { log: (msg) => Effect.void })
// ^? Context.Context<Logger>
Context.add [STABLE]
Добавляет сервис в контекст, возвращая новый контекст.
declare const add: <T extends Tag<any, any>>(
self: Context<any>,
tag: T,
service: Tag.Service<T>
) => Context<any | Tag.Identifier<T>>
// Использование
const context = Context.empty().pipe(
// ^? Context.Context<Database | Logger>
Context.add(Logger, loggerImpl),
Context.add(Database, databaseImpl),
)
Context.get [STABLE]
Извлекает сервис из контекста по тегу.
declare const get: <T extends Tag<any, any>>(
self: Context<Tag.Identifier<T>>,
tag: T
) => Tag.Service<T>
const program: Effect.Effect<void, never, Logger | SpecialNumber> = Effect.gen(function* () {
const context = yield* Effect.context<SpecialNumber | Logger>()
const logger = Context.get(context, Logger)
const specialNumber = Context.get(context, SpecialNumber)
yield* logger.log(`The special number is ${specialNumber}`)
})
При использовании данного подхода Context.Reference предоставляется, но не убирается из типа эффекта. Но данный подход является лучшей практикой, так как является контрактом того какие сервисы вам нужны
Context.getOption [STABLE]
Безопасно извлекает сервис, возвращая Option.
declare const getOption: <T extends Tag<any, any>>(
self: Context<any>,
tag: T
) => Option<Tag.Service<T>>
// Использование
const program = Effect.gen(function* () {
const context = yield* Effect.context<SpecialNumber | Logger>()
const logger = Context.getOption(context, Logger)
const specialNumber = Context.get(context, SpecialNumber)
if (Option.isSome(logger)) {
yield* logger.value.log(`The special number is ${specialNumber}`)
} else {
console.log(`The special number is ${specialNumber}`)
}
})
Context.merge [STABLE]
Объединяет два контекста.
declare const merge: <R1, R2>(
self: Context<R1>,
that: Context<R2>
) => Context<R1 | R2>
class Port extends Context.Tag("Port")<Port, { PORT: number }>() {}
class Timeout extends Context.Tag("Timeout")<Timeout, { TIMEOUT: number }>() {}
const portContext = Context.make(Port, { PORT: 8080 })
const timeoutContext = Context.make(Timeout, { TIMEOUT: 5000 })
const Combined = Context.merge(portContext, timeoutContext)
// ^? Context.Context<Port | Timeout>
Context.omit [STABLE]
Удаляет сервис из контекста.
declare const omit: <T extends Tag<any, any>>(
tag: T
) => <R>(self: Context<R>) => Context<Exclude<R, Tag.Identifier<T>>>
class Port extends Context.Tag("Port")<Port, { PORT: number }>() {}
class Timeout extends Context.Tag("Timeout")<Timeout, { TIMEOUT: number }>() {}
const portContext = Context.make(Port, { PORT: 8080 })
const timeoutContext = Context.make(Timeout, { TIMEOUT: 5000 })
const Combined = Context.merge(portContext, timeoutContext)
const omit = Combined.pipe(Context.omit(Port))
// ^? Context.Context<Timeout>
Context.pick [STABLE]
Создаёт контекст только с указанными сервисами.
declare const pick: <S extends Tag<any, any>[]>(
...tags: S
) => <R>(self: Context<R>) => Context<...>
const pick = Combined.pipe(Context.pick(Port))
// ^? Context.Context<Port>
Tag.Service [STABLE]
Utility type для извлечения типа сервиса из тега.
type Service<T> = T extends Tag<any, infer S> ? S : never
// Использование
type LoggerService = Context.Tag.Service<typeof Logger>
Tag.Identifier [STABLE]
Utility type для извлечения идентификатора тега.
type Identifier<T> = T extends Tag<infer I, any> ? I : never
// Использование
type LoggerId = Context.Tag.Identifier<typeof Logger>
Примеры
Пример 1: Базовое создание и использование тегов
// Определяем сервис логирования
class Logger extends Context.Tag("app/Logger")<
Logger,
{
readonly info: (message: string) => Effect.Effect<void>
readonly error: (message: string, error?: unknown) => Effect.Effect<void>
readonly debug: (message: string) => Effect.Effect<void>
}
>() {}
// Определяем сервис конфигурации
class Config extends Context.Tag("app/Config")<
Config,
{
readonly logLevel: "debug" | "info" | "error"
readonly appName: string
readonly version: string
}
>() {}
// Программа использует оба сервиса
const program = Effect.gen(function* () {
const logger = yield* Logger
const config = yield* Config
yield* logger.info(`Starting ${config.appName} v${config.version}`)
yield* logger.debug("Debug mode enabled")
return "Application started"
})
// Тип программы: Effect<string, never, Logger | Config>
// TypeScript знает, что нужны Logger и Config
Пример 2: Построение Context вручную
// Теги сервисов
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}
class Cache extends Context.Tag("Cache")<
Cache,
{
readonly get: (key: string) => Effect.Effect<string | null>
readonly set: (key: string, value: string) => Effect.Effect<void>
}
>() {}
class Metrics extends Context.Tag("Metrics")<
Metrics,
{ readonly increment: (name: string) => Effect.Effect<void> }
>() {}
// Реализации сервисов
const databaseImpl = {
query: (sql: string) => Effect.succeed([{ id: 1, name: "test" }] as const)
}
const cacheImpl = {
get: (key: string) => Effect.succeed(null),
set: (key: string, value: string) => Effect.void
}
const metricsImpl = {
increment: (name: string) => Effect.sync(() => console.log(`Metric: ${name}`))
}
// Построение контекста
const applicationContext = Context.empty().pipe(
Context.add(Database, databaseImpl),
Context.add(Cache, cacheImpl),
Context.add(Metrics, metricsImpl)
)
// Извлечение сервисов из контекста
const db = Context.get(applicationContext, Database)
const cache = Context.get(applicationContext, Cache)
const metrics = Context.get(applicationContext, Metrics)
// Безопасное извлечение
const maybeMetrics = Context.getOption(applicationContext, Metrics) // Option.none()
Пример 3: GenericTag для быстрого прототипирования
// Интерфейсы сервисов
interface HttpClient {
readonly get: (url: string) => Effect.Effect<unknown, Error>
readonly post: (url: string, body: unknown) => Effect.Effect<unknown, Error>
}
interface JsonParser {
readonly parse: <T>(json: string) => Effect.Effect<T, SyntaxError>
readonly stringify: (value: unknown) => Effect.Effect<string>
}
// Создание тегов через GenericTag
const HttpClient = Context.GenericTag<HttpClient>("HttpClient")
const JsonParser = Context.GenericTag<JsonParser>("JsonParser")
// Использование
const fetchJson = <T>(url: string): Effect.Effect<T, Error | SyntaxError, HttpClient | JsonParser> =>
Effect.gen(function* () {
const http = yield* HttpClient
const parser = yield* JsonParser
const response = yield* http.get(url)
const json = JSON.stringify(response)
return yield* parser.parse<T>(json)
})
Пример 4: Иерархия сервисов с наследованием тегов
// Базовый интерфейс репозитория
interface Repository<T> {
readonly findById: (id: string) => Effect.Effect<T | null>
readonly findAll: () => Effect.Effect<ReadonlyArray<T>>
readonly save: (entity: T) => Effect.Effect<T>
readonly delete: (id: string) => Effect.Effect<boolean>
}
// Модели данных
interface User {
readonly id: string
readonly email: string
readonly name: string
}
interface Product {
readonly id: string
readonly name: string
readonly price: number
}
// Специализированные теги репозиториев
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
Repository<User> & {
readonly findByEmail: (email: string) => Effect.Effect<User | null>
}
>() {}
class ProductRepository extends Context.Tag("ProductRepository")<
ProductRepository,
Repository<Product> & {
readonly findByPriceRange: (min: number, max: number) => Effect.Effect<ReadonlyArray<Product>>
}
>() {}
// Использование
const findUserProducts = (userId: string) => Effect.gen(function* () {
const userRepo = yield* UserRepository
const productRepo = yield* ProductRepository
const user = yield* userRepo.findById(userId)
if (!user) return []
const products = yield* productRepo.findAll()
return products
})
// Тип: Effect<ReadonlyArray<Product>, never, UserRepository | ProductRepository>
Пример 5: Объединение и манипуляция контекстами
// Определение тегов
class Auth extends Context.Tag("Auth")<
Auth,
{ readonly currentUserId: string | null }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly apiUrl: string }
>() {}
class Telemetry extends Context.Tag("Telemetry")<
Telemetry,
{ readonly track: (event: string) => Effect.Effect<void> }
>() {}
// Создание частичных контекстов
const authContext = Context.make(Auth, { currentUserId: "user-123" })
const loggingContext = Context.empty().pipe(
Context.add(Logger, { log: (msg) => Effect.sync(() => console.log(msg)) }),
Context.add(Config, { apiUrl: "https://api.example.com" })
)
const telemetryContext = Context.make(Telemetry, {
track: (event) => Effect.sync(() => console.log(`Event: ${event}`))
})
// Объединение контекстов
const fullContext = Context.merge(
Context.merge(authContext, loggingContext),
telemetryContext
)
// Выбор подмножества
const minimalContext = fullContext.pipe(
Context.pick(Auth, Logger)
)
// Удаление сервиса
const withoutTelemetry = fullContext.pipe(
Context.omit(Telemetry)
)
// Проверка наличия сервиса
const hasAuth = Option.isSome(Context.getOption(fullContext, Auth)) // true
const hasDatabase = Option.isSome(Context.getOption(fullContext, Database)) // false (не определён)
Пример 6: Типобезопасное расширение контекста
// Базовые сервисы
class Clock extends Context.Tag("Clock")<
Clock,
{ readonly now: () => Effect.Effect<Date> }
>() {}
class Random extends Context.Tag("Random")<
Random,
{ readonly nextInt: () => Effect.Effect<number> }
>() {}
// Функция, расширяющая контекст
const withTimestamp = <R, E, A>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R | Clock> =>
Effect.gen(function* () {
const clock = yield* Clock
const timestamp = yield* clock.now()
console.log(`[${timestamp.toISOString()}] Running effect...`)
return yield* effect
})
// Функция, требующая Random
const generateCode = Effect.gen(function* () {
const random = yield* Random
const n1 = yield* random.nextInt()
const n2 = yield* random.nextInt()
return `${n1}-${n2}`
})
// Комбинация: требует Random | Clock
const generateTimestampedCode = withTimestamp(generateCode)
// Тип: Effect<string, never, Random | Clock>
// Предоставление всех зависимостей
const program = generateTimestampedCode.pipe(
Effect.provideService(Random, { nextInt: () => Effect.succeed(Math.floor(Math.random() * 10000)) }),
Effect.provideService(Clock, { now: () => Effect.succeed(new Date()) })
)
// Теперь можно запустить
Effect.runPromise(program).then(console.log)
Упражнения
Создание простых тегов
Создайте теги для следующих сервисов:
Randomizerс методомnextNumber(): Effect<number>Timerс методомsleep(ms: number): Effect<void>IdGeneratorс методомgenerate(): Effect<string>
// Ваш код здесь
// Проверка типов:
// const program: Effect<string, never, Randomizer | Timer | IdGenerator>
class Randomizer extends Context.Tag("Randomizer")<
Randomizer,
{ readonly nextNumber: () => Effect.Effect<number> }
>() {}
class Timer extends Context.Tag("Timer")<
Timer,
{ readonly sleep: (ms: number) => Effect.Effect<void> }
>() {}
class IdGenerator extends Context.Tag("IdGenerator")<
IdGenerator,
{ readonly generate: () => Effect.Effect<string> }
>() {}
// Проверка
const program = Effect.gen(function* () {
const random = yield* Randomizer
const timer = yield* Timer
const idGen = yield* IdGenerator
const num = yield* random.nextNumber()
yield* timer.sleep(100)
const id = yield* idGen.generate()
return `${id}-${num}`
})
// Тип: Effect<string, never, Randomizer | Timer | IdGenerator>Построение контекста
Создайте контекст, содержащий реализации для Logger и Config:
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly debug: boolean; readonly maxRetries: number }
>() {}
// Создайте контекст с обоими сервисами
const context = // Ваш код здесь
// Извлеките Logger из контекста
const logger = // Ваш код здесь
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly debug: boolean; readonly maxRetries: number }
>() {}
// Создание контекста
const context = Context.empty().pipe(
Context.add(Logger, {
log: (msg) => Effect.sync(() => console.log(msg))
}),
Context.add(Config, {
debug: true,
maxRetries: 3
})
)
// Извлечение сервиса
const logger = Context.get(context, Logger)
// Использование
const program = Effect.gen(function* () {
yield* logger.log("Hello from context!")
})GenericTag
Перепишите следующие теги с использованием GenericTag:
class UserService extends Context.Tag("UserService")<
UserService,
{ readonly findUser: (id: string) => Effect.Effect<User | null> }
>() {}
// Перепишите через GenericTag:
// const UserService = ...
interface User {
readonly id: string
readonly name: string
}
interface UserService {
readonly findUser: (id: string) => Effect.Effect<User | null>
}
// Переписываем через GenericTag
const UserService = Context.GenericTag<UserService>("UserService")
// Использование
const program = Effect.gen(function* () {
const service = yield* UserService
const user = yield* service.findUser("123")
return user
})Комбинирование контекстов
Реализуйте функцию createApplicationContext, которая:
- Принимает три отдельных контекста:
dbContext,cacheContext,loggingContext - Объединяет их в один
- Добавляет дополнительный сервис
Metrics
// Определения тегов
class Database extends Context.Tag("Database")<Database, { query: (sql: string) => Effect.Effect<unknown[]> }>() {}
class Cache extends Context.Tag("Cache")<Cache, { get: (k: string) => Effect.Effect<string | null> }>() {}
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
class Metrics extends Context.Tag("Metrics")<Metrics, { record: (n: string, v: number) => Effect.Effect<void> }>() {}
const createApplicationContext = (
dbContext: Context.Context<Database>,
cacheContext: Context.Context<Cache>,
loggingContext: Context.Context<Logger>
): Context.Context<Database | Cache | Logger | Metrics> => {
// Ваш код здесь
}
class Database extends Context.Tag("Database")<Database, { query: (sql: string) => Effect.Effect<unknown[]> }>() {}
class Cache extends Context.Tag("Cache")<Cache, { get: (k: string) => Effect.Effect<string | null> }>() {}
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
class Metrics extends Context.Tag("Metrics")<Metrics, { record: (n: string, v: number) => Effect.Effect<void> }>() {}
const createApplicationContext = (
dbContext: Context.Context<Database>,
cacheContext: Context.Context<Cache>,
loggingContext: Context.Context<Logger>
): Context.Context<Database | Cache | Logger | Metrics> => {
// Создаём реализацию Metrics
const metricsImpl = {
record: (name: string, value: number) =>
Effect.sync(() => console.log(`Metric ${name}: ${value}`))
}
// Объединяем все контексты и добавляем Metrics
return Context.empty().pipe(
Context.add(Metrics, metricsImpl),
Context.merge(dbContext),
Context.merge(cacheContext),
Context.merge(loggingContext)
)
}Условная логика с контекстом
Реализуйте функцию getServiceOrDefault, которая:
- Пытается получить сервис из контекста
- Если сервис отсутствует, возвращает значение по умолчанию
const getServiceOrDefault = <T extends Context.Tag<any, any>>(
context: Context.Context<any>,
tag: T,
defaultValue: Context.Tag.Service<T>
): Context.Tag.Service<T> => {
// Ваш код здесь
}
const getServiceOrDefault = <T extends Context.Tag<any, any>>(
context: Context.Context<any>,
tag: T,
defaultValue: Context.Tag.Service<T>
): Context.Tag.Service<T> => {
const maybeService = Context.getOption(context, tag)
return Option.isSome(maybeService) ? maybeService.value : defaultValue
}
// Использование
const loggerDefault = {
log: (msg: string) => Effect.sync(() => console.log(`[DEFAULT] ${msg}`))
}
const context = Context.empty()
const logger = getServiceOrDefault(context, Logger, loggerDefault)Фабрика тегов
Создайте generic функцию createRepositoryTag, которая создаёт тег для репозитория любой сущности:
interface Entity {
readonly id: string
}
interface Repository<T extends Entity> {
readonly findById: (id: string) => Effect.Effect<T | null>
readonly save: (entity: T) => Effect.Effect<T>
readonly delete: (id: string) => Effect.Effect<boolean>
}
// Реализуйте функцию
const createRepositoryTag = <T extends Entity>(
name: string
): Context.Tag<Repository<T>, Repository<T>> => {
// Ваш код здесь
}
// Использование:
interface User extends Entity { name: string; email: string }
const UserRepo = createRepositoryTag<User>("UserRepository")
interface Entity {
readonly id: string
}
interface Repository<T extends Entity> {
readonly findById: (id: string) => Effect.Effect<T | null>
readonly save: (entity: T) => Effect.Effect<T>
readonly delete: (id: string) => Effect.Effect<boolean>
}
const createRepositoryTag = <T extends Entity>(
name: string
): Context.Tag<Repository<T>, Repository<T>> => {
return Context.GenericTag<Repository<T>>(`Repository.${name}`)
}
// Использование
interface User extends Entity { name: string; email: string }
interface Product extends Entity { name: string; price: number }
const UserRepo = createRepositoryTag<User>("User")
const ProductRepo = createRepositoryTag<Product>("Product")
// Программа
const program = Effect.gen(function* () {
const userRepo = yield* UserRepo
const productRepo = yield* ProductRepo
const user = yield* userRepo.findById("1")
const product = yield* productRepo.findById("2")
return { user, product }
})Middleware с контекстом
Реализуйте паттерн middleware, который обогащает контекст:
class RequestId extends Context.Tag("RequestId")<RequestId, { readonly id: string }>() {}
class Timestamp extends Context.Tag("Timestamp")<Timestamp, { readonly value: Date }>() {}
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
// Middleware добавляет RequestId и Timestamp к любому эффекту
const withRequestContext = <R, E, A>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, Exclude<R, RequestId | Timestamp> | Logger> => {
// Ваш код здесь
// Должен:
// 1. Генерировать уникальный RequestId
// 2. Фиксировать Timestamp
// 3. Логировать начало и конец выполнения
// 4. Предоставлять RequestId и Timestamp в эффект
}
class RequestId extends Context.Tag("RequestId")<RequestId, { readonly id: string }>() {}
class Timestamp extends Context.Tag("Timestamp")<Timestamp, { readonly value: Date }>() {}
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
const withRequestContext = <R, E, A>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, Exclude<R, RequestId | Timestamp> | Logger> =>
Effect.gen(function* () {
const logger = yield* Logger
const requestId = crypto.randomUUID()
const timestamp = new Date()
yield* logger.log(`[${requestId}] Starting at ${timestamp.toISOString()}`)
const result = yield* effect.pipe(
Effect.provideService(RequestId, { id: requestId }),
Effect.provideService(Timestamp, { value: timestamp })
)
yield* logger.log(`[${requestId}] Completed`)
return result
})Type-safe конфигурация окружений
Создайте систему для type-safe конфигурации разных окружений:
// Определите типы для разных окружений
type Environment = "development" | "staging" | "production"
// Каждое окружение имеет свой набор сервисов
// Development: LocalDatabase, ConsoleLogger, MockPayments
// Staging: StagingDatabase, FileLogger, TestPayments
// Production: ProductionDatabase, CloudLogger, LivePayments
// Реализуйте type-safe функцию создания контекста:
const createEnvironmentContext = <E extends Environment>(
env: E
): Context.Context</* правильный тип для окружения */> => {
// Ваш код здесь
}
// Контекст должен иметь разные типы для разных окружений
const devContext = createEnvironmentContext("development")
const prodContext = createEnvironmentContext("production")
// Сервисы
class LocalDatabase extends Context.Tag("LocalDatabase")<LocalDatabase, { query: (sql: string) => Effect.Effect<unknown[]> }>() {}
class StagingDatabase extends Context.Tag("StagingDatabase")<StagingDatabase, { query: (sql: string) => Effect.Effect<unknown[]> }>() {}
class ProductionDatabase extends Context.Tag("ProductionDatabase")<ProductionDatabase, { query: (sql: string) => Effect.Effect<unknown[]> }>() {}
class ConsoleLogger extends Context.Tag("ConsoleLogger")<ConsoleLogger, { log: (msg: string) => Effect.Effect<void> }>() {}
class FileLogger extends Context.Tag("FileLogger")<FileLogger, { log: (msg: string) => Effect.Effect<void> }>() {}
class CloudLogger extends Context.Tag("CloudLogger")<CloudLogger, { log: (msg: string) => Effect.Effect<void> }>() {}
// Маппинг окружений к типам
type EnvironmentConfig = {
development: LocalDatabase | ConsoleLogger
staging: StagingDatabase | FileLogger
production: ProductionDatabase | CloudLogger
}
const createEnvironmentContext = <E extends keyof EnvironmentConfig>(
env: E
): Context.Context<EnvironmentConfig[E]> => {
switch (env) {
case "development":
return Context.empty().pipe(
Context.add(LocalDatabase, { query: () => Effect.succeed([]) }),
Context.add(ConsoleLogger, { log: () => Effect.void })
) as Context.Context<EnvironmentConfig[E]>
case "staging":
return Context.empty().pipe(
Context.add(StagingDatabase, { query: () => Effect.succeed([]) }),
Context.add(FileLogger, { log: () => Effect.void })
) as Context.Context<EnvironmentConfig[E]>
case "production":
return Context.empty().pipe(
Context.add(ProductionDatabase, { query: () => Effect.succeed([]) }),
Context.add(CloudLogger, { log: () => Effect.void })
) as Context.Context<EnvironmentConfig[E]>
default:
throw new Error(`Unknown environment: ${env}`)
}
}Контекст с версионированием
Реализуйте систему версионирования сервисов в контексте:
// Тег с версией
interface VersionedService<T> {
readonly version: string
readonly service: T
}
// Создайте утилиты для:
// 1. Создания версионированного тега
// 2. Проверки совместимости версий
// 3. Миграции между версиями
const createVersionedTag = <T>(
name: string,
version: string
): Context.Tag<VersionedService<T>, VersionedService<T>> => {
// Ваш код здесь
}
const checkVersionCompatibility = (
required: string,
provided: string
): boolean => {
// Ваш код здесь (semver совместимость)
}
const migrateService = <T>(
context: Context.Context<any>,
fromTag: Context.Tag<VersionedService<T>, VersionedService<T>>,
toTag: Context.Tag<VersionedService<T>, VersionedService<T>>,
migrator: (old: T) => T
): Context.Context<any> => {
// Ваш код здесь
}
interface VersionedService<T> {
readonly version: string
readonly service: T
}
const createVersionedTag = <T>(
name: string,
version: string
): Context.Tag<VersionedService<T>, VersionedService<T>> => {
return Context.GenericTag<VersionedService<T>>(`${name}@v${version}`)
}
// Простая semver проверка (major.minor.patch)
const checkVersionCompatibility = (
required: string,
provided: string
): boolean => {
const parse = (v: string) => v.split(".").map(Number)
const [rMajor, rMinor] = parse(required)
const [pMajor, pMinor] = parse(provided)
// Major должен совпадать, minor >= required
return pMajor === rMajor && pMinor >= rMinor
}
const migrateService = <T>(
context: Context.Context<any>,
fromTag: Context.Tag<VersionedService<T>, VersionedService<T>>,
toTag: Context.Tag<VersionedService<T>, VersionedService<T>>,
migrator: (old: T) => T
): Context.Context<any> => {
const maybeService = Context.getOption(context, fromTag)
if (Option.isNone(maybeService)) {
return context
}
const oldService = maybeService.value
const newService: VersionedService<T> = {
version: "2.0.0", // Новая версия
service: migrator(oldService.service)
}
return context.pipe(
Context.omit(fromTag),
Context.add(toTag, newService)
)
}Заключение
В этой статье мы изучили:
- 📖 Context — immutable коллекция для хранения сервисов
- 📖 Tag — уникальный типизированный идентификатор сервиса
- 📖 GenericTag — низкоуровневый API для создания тегов
- 📖 Связь с Reader Monad и принципом Inversion of Control
- 📖 Операции над Context:
add,get,merge,pick,omit
💡 Ключевой takeaway: Context и Tag — это фундамент системы dependency injection в Effect. Понимание этих концепций критически важно для построения модульных, тестируемых приложений.