Effect Курс Context, Tag и GenericTag

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:

  1. Иммутабельность — Context нельзя изменить, можно только создать новый
  2. Type-safety — TypeScript гарантирует соответствие тегов и сервисов
  3. Composability — Context можно объединять через функцию add
  4. Runtime lookup — O(1) поиск сервиса по тегу

Что такое Tag?

Tag — это уникальный идентификатор сервиса в системе Effect. Tag выполняет две критические функции:

  1. Идентификация на уровне типов — TypeScript знает, какой тип сервиса связан с тегом
  2. Идентификация в 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>
Упражнение

Построение контекста

Легко

Создайте контекст, содержащий реализации для 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 = // Ваш код здесь
Упражнение

GenericTag

Легко

Перепишите следующие теги с использованием GenericTag:

class UserService extends Context.Tag("UserService")<
  UserService,
  { readonly findUser: (id: string) => Effect.Effect<User | null> }
>() {}

// Перепишите через GenericTag:
// const UserService = ...
Упражнение

Комбинирование контекстов

Средне

Реализуйте функцию createApplicationContext, которая:

  1. Принимает три отдельных контекста: dbContext, cacheContext, loggingContext
  2. Объединяет их в один
  3. Добавляет дополнительный сервис 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> => {
  // Ваш код здесь
}
Упражнение

Условная логика с контекстом

Средне

Реализуйте функцию getServiceOrDefault, которая:

  1. Пытается получить сервис из контекста
  2. Если сервис отсутствует, возвращает значение по умолчанию

const getServiceOrDefault = <T extends Context.Tag<any, any>>(
  context: Context.Context<any>,
  tag: T,
  defaultValue: Context.Tag.Service<T>
): Context.Tag.Service<T> => {
  // Ваш код здесь
}
Упражнение

Фабрика тегов

Средне

Создайте 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")
Упражнение

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 в эффект
}
Упражнение

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")
Упражнение

Контекст с версионированием

Сложно

Реализуйте систему версионирования сервисов в контексте:


// Тег с версией
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> => {
  // Ваш код здесь
}

Заключение

В этой статье мы изучили:

  • 📖 Context — immutable коллекция для хранения сервисов
  • 📖 Tag — уникальный типизированный идентификатор сервиса
  • 📖 GenericTag — низкоуровневый API для создания тегов
  • 📖 Связь с Reader Monad и принципом Inversion of Control
  • 📖 Операции над Context: add, get, merge, pick, omit

💡 Ключевой takeaway: Context и Tag — это фундамент системы dependency injection в Effect. Понимание этих концепций критически важно для построения модульных, тестируемых приложений.