Предоставление сервисов
Как предоставлять сервисы в Effect.
Теория
Проблема неудовлетворённых зависимостей
Когда Effect имеет непустой параметр R (Requirements), его нельзя выполнить напрямую:
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
// Программа требует Logger
const program: Effect.Effect<void, never, Logger> = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Hello!")
})
// ❌ Ошибка компиляции: Argument of type 'Effect<void, never, Logger>'
// is not assignable to parameter of type 'Effect<void, never, never>'
Effect.runSync(program)
TypeScript не позволит запустить программу, пока все зависимости не будут удовлетворены. Это ключевое преимущество Effect — compile-time проверка зависимостей.
Как работает предоставление сервисов
Когда вы вызываете Effect.provideService, Effect:
- Создаёт новый
Contextс предоставленным сервисом - Выполняет эффект в этом контексте
- Убирает требование сервиса из типа
R
📊 Визуализация процесса:
┌─────────────────────────────────────────────────────────────┐
│ Effect<A, E, Logger | Database | Config> │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ provideService(Logger, loggerImpl) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Effect<A, E, Database | Config> │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ provideService(Database, databaseImpl) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Effect<A, E, Config> │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ provideService(Config, configImpl) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Effect<A, E, never> ← Можно запустить! │
└─────────────────────────────────────────────────────────────┘
Основные способы предоставления сервисов
Effect предлагает несколько механизмов для предоставления сервисов:
| Функция | Назначение | Когда использовать |
|---|---|---|
Effect.provideService | Предоставить один сервис | Простые случаи |
Effect.provide | Предоставить Context или Layer | Универсальный способ |
Effect.provideServiceEffect | Предоставить сервис через Effect | Когда создание требует эффектов |
Effect.provideService: базовый механизм
Effect.provideService — самый прямой способ предоставить сервис:
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Starting application...")
return "done"
})
// Предоставляем реализацию
const runnable = program.pipe(
Effect.provideService(Logger, {
log: (msg) => Effect.sync(() => console.log(`[LOG] ${msg}`))
})
)
// Теперь тип: Effect<string, never, never>
// Можно запустить!
Effect.runSync(runnable)
💡 Порядок предоставления не важен:
// Эти варианты эквивалентны:
// Вариант 1: Logger, затем Database
const v1 = program.pipe(
Effect.provideService(Logger, loggerImpl),
Effect.provideService(Database, databaseImpl)
)
// Вариант 2: Database, затем Logger
const v2 = program.pipe(
Effect.provideService(Database, databaseImpl),
Effect.provideService(Logger, loggerImpl)
)
Effect.provide с Context: множественные сервисы
Для предоставления нескольких сервисов одновременно используйте Effect.provide с Context:
// Определяем сервисы
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly apiUrl: string; readonly timeout: number }
>() {}
class Metrics extends Context.Tag("Metrics")<
Metrics,
{ readonly increment: (name: string) => Effect.Effect<void> }
>() {}
// Программа требует все три сервиса
const program: Effect.Effect<void, never, Logger | Config | Metrics> =
Effect.gen(function* () {
const logger = yield* Logger
const config = yield* Config
const metrics = yield* Metrics
yield* logger.log(`Connecting to ${config.apiUrl}`)
yield* metrics.increment("app.startup")
})
// Создаём Context со всеми сервисами
const appContext = Context.empty().pipe(
Context.add(Logger, {
log: (msg) => Effect.sync(() => console.log(msg))
}),
Context.add(Config, {
apiUrl: "https://api.example.com",
timeout: 5000
}),
Context.add(Metrics, {
increment: (name) => Effect.sync(() => console.log(`Metric: ${name}`))
})
)
// Предоставляем весь контекст одним вызовом
const runnable = Effect.provide(program, appContext)
Effect.runSync(runnable)
Effect.provide с Layer: рекомендуемый подход
Layer — это более мощный механизм для построения сервисов с зависимостями:
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
// Layer для Logger
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => console.log(`[LOG] ${msg}`))
})
// Layer для Database (зависит от Logger)
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const logger = yield* Logger
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(`Executing: ${sql}`)
return [{ id: 1, name: "test" }]
})
}
})
)
// Композиция слоёв
const AppLayer = Layer.merge(
LoggerLive,
Layer.provide(DatabaseLive, LoggerLive)
)
// Использование
const program = Effect.gen(function* () {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const runnable = Effect.provide(program, AppLayer)
Effect.runPromise(runnable)
⚠️ Важно: На один Effect должен быть один Effect.provide. Это связанно с тем что под капотом Effect строит карту зависимостей (мемоизацию, ссылки и так далее) и её перезапись ухудшит производительность и может вызвать ошибки. ⚠️ Важно: Layer — тема следующего модуля. Здесь показано для полноты картины.
Effect.provideServiceEffect: динамическое создание
Когда создание сервиса требует выполнения эффектов:
class ApiClient extends Context.Tag("ApiClient")<
ApiClient,
{
readonly get: (path: string) => Effect.Effect<unknown, Error>
readonly post: (path: string, body: unknown) => Effect.Effect<unknown, Error>
}
>() {}
const program = Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.get("/users")
})
// Создание сервиса требует чтения конфигурации
const createApiClient = Effect.gen(function* () {
const baseUrl = yield* Config.string("API_BASE_URL")
const timeout = yield* Config.number("API_TIMEOUT").pipe(
Config.withDefault(5000)
)
return {
get: (path: string) => Effect.tryPromise({
try: () => fetch(`${baseUrl}${path}`, {
signal: AbortSignal.timeout(timeout)
}).then(r => r.json()),
catch: (e) => new Error(`GET failed: ${e}`)
}),
post: (path: string, body: unknown) => Effect.tryPromise({
try: () => fetch(`${baseUrl}${path}`, {
method: "POST",
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeout)
}).then(r => r.json()),
catch: (e) => new Error(`POST failed: ${e}`)
})
}
})
// provideServiceEffect позволяет использовать Effect для создания сервиса
const runnable = program.pipe(
Effect.provideServiceEffect(ApiClient, createApiClient)
)
Частичное предоставление сервисов
Вы можете предоставить только часть требуемых сервисов:
class A extends Context.Tag("A")<A, { value: number }>() {}
class B extends Context.Tag("B")<B, { value: string }>() {}
class C extends Context.Tag("C")<C, { value: boolean }>() {}
// Требует A, B, C
const program: Effect.Effect<string, never, A | B | C> = Effect.gen(function* () {
const a = yield* A
const b = yield* B
const c = yield* C
return `${a.value}-${b.value}-${c.value}`
})
// Предоставляем только A
const partial1 = program.pipe(
Effect.provideService(A, { value: 42 })
)
// Тип: Effect<string, never, B | C>
// Предоставляем A и B
const partial2 = program.pipe(
Effect.provideService(A, { value: 42 }),
Effect.provideService(B, { value: "hello" })
)
// Тип: Effect<string, never, C>
// Предоставляем все
const complete = program.pipe(
Effect.provideService(A, { value: 42 }),
Effect.provideService(B, { value: "hello" }),
Effect.provideService(C, { value: true })
)
// Тип: Effect<string, never, never>
Концепция ФП: Dependency Injection и Reader
Reader Pattern
В функциональном программировании Reader — это паттерн для передачи конфигурации через вычисления:
// Reader<R, A> = (env: R) => A
type Reader<R, A> = (env: R) => A
// "runReader" — предоставляет окружение
const runReader = <R, A>(reader: Reader<R, A>, env: R): A => reader(env)
// Пример
interface Env {
readonly apiUrl: string
readonly token: string
}
const fetchUser: Reader<Env, Promise<User>> = (env) =>
fetch(`${env.apiUrl}/user`, {
headers: { Authorization: `Bearer ${env.token}` }
}).then(r => r.json())
// Использование
const user = runReader(fetchUser, {
apiUrl: "https://api.example.com",
token: "secret"
})
От Reader к Effect
Effect расширяет Reader:
// Reader<R, A> ≈ Effect<A, never, R>
// Но Effect добавляет:
// - Ошибки (E)
// - Ленивость
// - Конкурентность (Fibers)
// - Ресурсы (Scope)
// - Прерываемость
class Env extends Context.Tag("Env")<Env, { apiUrl: string; token: string }>() {}
// Reader в Effect-стиле
const fetchUser: Effect.Effect<User, Error, Env> = Effect.gen(function* () {
const env = yield* Env
return yield* Effect.tryPromise({
try: () => fetch(`${env.apiUrl}/user`, {
headers: { Authorization: `Bearer ${env.token}` }
}).then(r => r.json()),
catch: (e) => new Error(`Fetch failed: ${e}`)
})
})
// "runReader" = Effect.provideService
const user = Effect.runPromise(
fetchUser.pipe(
Effect.provideService(Env, {
apiUrl: "https://api.example.com",
token: "secret"
})
)
)
Algebraic Effects интерпретация
С точки зрения algebraic effects, provideService — это handler для эффекта:
// Эффект "требует Logger" — это operation
const logEffect = Effect.gen(function* () {
const logger = yield* Logger // "perform Log operation"
yield* logger.log("message")
})
// provideService — это handler
const handled = logEffect.pipe(
Effect.provideService(Logger, {
log: (msg) => Effect.sync(() => console.log(msg)) // interpretation
})
)
// Можно представить как:
// handle logEffect with
// | Logger.log(msg) -> resume(console.log(msg))
Локальность vs Глобальность
В отличие от глобального DI (как в Spring/NestJS), Effect предоставляет локальный DI:
// Глобальный DI (традиционный подход):
// @Injectable()
// class UserService {
// constructor(private db: Database) {} // Глобально зарегистрирован
// }
// Effect: локальный DI
const userServiceLogic = Effect.gen(function* () {
const db = yield* Database
// ...
})
// Разные части приложения могут использовать разные реализации:
const production = userServiceLogic.pipe(
Effect.provideService(Database, productionDb)
)
const test = userServiceLogic.pipe(
Effect.provideService(Database, testDb)
)
const staging = userServiceLogic.pipe(
Effect.provideService(Database, stagingDb)
)
API Reference
Effect.provideService [STABLE]
Предоставляет реализацию одного сервиса.
declare const provideService: {
// Data-last (curried)
<T extends Tag<any, any>>(
tag: T,
service: Tag.Service<T>
): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, Exclude<R, Tag.Identifier<T>>>
// Data-first
<A, E, R, T extends Tag<any, any>>(
self: Effect<A, E, R>,
tag: T,
service: Tag.Service<T>
): Effect<A, E, Exclude<R, Tag.Identifier<T>>>
}
Пример:
const runnable = program.pipe(
Effect.provideService(Logger, { log: (m) => Effect.sync(() => console.log(m)) })
)
Effect.provideServiceEffect [STABLE]
Предоставляет сервис через Effect.
declare const provideServiceEffect: {
<T extends Tag<any, any>, E1, R1>(
tag: T,
effect: Effect<Tag.Service<T>, E1, R1>
): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E | E1, R1 | Exclude<R, Tag.Identifier<T>>>
}
Пример:
const createLogger = Effect.gen(function* () {
const config = yield* Config
return { log: (m: string) => Effect.sync(() => console.log(`[${config.prefix}] ${m}`)) }
})
const runnable = program.pipe(
Effect.provideServiceEffect(Logger, createLogger)
)
// Тип включает Config в requirements
Effect.provide [STABLE]
Универсальная функция для предоставления Context, Layer, или Runtime.
declare const provide: {
// С Context
<R2>(context: Context<R2>): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, Exclude<R, R2>>
// С Layer
<ROut, E2, RIn>(
layer: Layer<ROut, E2, RIn>
): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E | E2, RIn | Exclude<R, ROut>>
// С Runtime
<R2>(runtime: Runtime<R2>): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, Exclude<R, R2>>
}
Примеры:
// С Context
const withContext = Effect.provide(program, myContext)
// С Layer
const withLayer = Effect.provide(program, MyServiceLive)
// С Runtime
const withRuntime = Effect.provide(program, myRuntime)
Примеры
Пример 1: Пошаговое предоставление сервисов
// Сервисы
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly apiUrl: string; readonly debug: boolean }
>() {}
class HttpClient extends Context.Tag("HttpClient")<
HttpClient,
{ readonly get: (url: string) => Effect.Effect<unknown, Error> }
>() {}
// Программа требует все три сервиса
const fetchData = Effect.gen(function* () {
const logger = yield* Logger
const config = yield* Config
const http = yield* HttpClient
yield* logger.log(`Fetching from ${config.apiUrl}`)
const data = yield* http.get(`${config.apiUrl}/data`)
if (config.debug) {
yield* logger.log(`Received: ${JSON.stringify(data)}`)
}
return data
})
// Тип: Effect<unknown, Error, Logger | Config | HttpClient>
// Шаг 1: предоставляем Logger
const step1 = fetchData.pipe(
Effect.provideService(Logger, {
log: (msg) => Effect.sync(() => console.log(`[LOG] ${msg}`))
})
)
// Тип: Effect<unknown, Error, Config | HttpClient>
// Шаг 2: предоставляем Config
const step2 = step1.pipe(
Effect.provideService(Config, {
apiUrl: "https://api.example.com",
debug: true
})
)
// Тип: Effect<unknown, Error, HttpClient>
// Шаг 3: предоставляем HttpClient
const runnable = step2.pipe(
Effect.provideService(HttpClient, {
get: (url) => Effect.tryPromise({
try: () => fetch(url).then(r => r.json()),
catch: (e) => new Error(`HTTP error: ${e}`)
})
})
)
// Тип: Effect<unknown, Error, never>
// Теперь можно запустить!
Effect.runPromise(runnable).then(console.log)
Пример 2: Использование Context для множественных сервисов
// Определения сервисов
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User | null>
readonly save: (user: User) => Effect.Effect<User>
}
>() {}
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
readonly send: (to: string, subject: string, body: string) => Effect.Effect<void, EmailError>
}
>() {}
class NotificationService extends Context.Tag("NotificationService")<
NotificationService,
{
readonly notify: (userId: string, message: string) => Effect.Effect<void>
}
>() {}
// Бизнес-логика
const registerUser = (email: string, name: string) => Effect.gen(function* () {
const userRepo = yield* UserRepository
const emailService = yield* EmailService
const notifications = yield* NotificationService
// Создаём пользователя
const user = yield* userRepo.save({
id: crypto.randomUUID(),
email,
name
})
// Отправляем welcome email
yield* emailService.send(
email,
"Welcome!",
`Hello ${name}, welcome to our platform!`
)
// Уведомляем администраторов
yield* notifications.notify("admin", `New user registered: ${email}`)
return user
})
// Создаём полный контекст
const applicationContext = Context.empty().pipe(
Context.add(UserRepository, {
findById: (id) => Effect.succeed(null),
save: (user) => Effect.succeed(user)
}),
Context.add(EmailService, {
send: (to, subject, body) => Effect.sync(() => {
console.log(`Sending email to ${to}: ${subject}`)
})
}),
Context.add(NotificationService, {
notify: (userId, message) => Effect.sync(() => {
console.log(`Notifying ${userId}: ${message}`)
})
})
)
// Предоставляем весь контекст
const runnable = Effect.provide(
registerUser("alice@example.com", "Alice"),
applicationContext
)
Effect.runPromise(runnable).then(console.log)
Пример 3: Динамическое создание сервиса
// Сервис для работы с S3
class S3Client extends Context.Tag("S3Client")<
S3Client,
{
readonly putObject: (key: string, body: Uint8Array) => Effect.Effect<void, S3Error>
readonly getObject: (key: string) => Effect.Effect<Uint8Array, S3Error>
readonly deleteObject: (key: string) => Effect.Effect<void, S3Error>
}
>() {}
// Создание клиента требует конфигурации
const createS3Client = Effect.gen(function* () {
const region = yield* Config.string("AWS_REGION")
const bucket = yield* Config.string("S3_BUCKET")
const accessKey = yield* Config.string("AWS_ACCESS_KEY_ID")
const secretKey = yield* Config.string("AWS_SECRET_ACCESS_KEY")
console.log(`Initializing S3 client for bucket: ${bucket} in ${region}`)
// В реальности здесь была бы инициализация AWS SDK
return {
putObject: (key: string, body: Uint8Array) => Effect.gen(function* () {
console.log(`Uploading ${key} (${body.length} bytes) to ${bucket}`)
// await s3.putObject({ Bucket: bucket, Key: key, Body: body }).promise()
}),
getObject: (key: string) => Effect.gen(function* () {
console.log(`Downloading ${key} from ${bucket}`)
return new Uint8Array([1, 2, 3]) // Мок
}),
deleteObject: (key: string) => Effect.gen(function* () {
console.log(`Deleting ${key} from ${bucket}`)
})
}
})
// Программа использует S3Client
const uploadFile = (filename: string, content: string) => Effect.gen(function* () {
const s3 = yield* S3Client
const data = new TextEncoder().encode(content)
yield* s3.putObject(filename, data)
console.log(`File ${filename} uploaded successfully`)
})
// Предоставляем сервис через Effect
const runnable = uploadFile("test.txt", "Hello, World!").pipe(
Effect.provideServiceEffect(S3Client, createS3Client)
)
// Тип: Effect<void, S3Error | ConfigError, never>
// ConfigError может возникнуть при создании сервиса
Пример 4: Тестирование с подменой сервисов
// Сервис платёжной системы
class PaymentGateway extends Context.Tag("PaymentGateway")<
PaymentGateway,
{
readonly charge: (amount: number, cardToken: string) => Effect.Effect<PaymentResult, PaymentError>
readonly refund: (transactionId: string) => Effect.Effect<void, PaymentError>
}
>() {}
// Бизнес-логика
const processPayment = (orderId: string, amount: number, cardToken: string) =>
Effect.gen(function* () {
const gateway = yield* PaymentGateway
// Пытаемся списать деньги
const result = yield* gateway.charge(amount, cardToken)
console.log(`Payment for order ${orderId}: ${result.status}`)
return result
})
// === Production реализация ===
const productionPaymentGateway = {
charge: (amount: number, cardToken: string) =>
Effect.tryPromise({
try: async () => {
// Реальный вызов платёжного API
const response = await fetch("https://payment.example.com/charge", {
method: "POST",
body: JSON.stringify({ amount, cardToken })
})
return response.json() as Promise<PaymentResult>
},
catch: (e) => new PaymentError(`Charge failed: ${e}`)
}),
refund: (transactionId: string) =>
Effect.tryPromise({
try: () => fetch(`https://payment.example.com/refund/${transactionId}`, { method: "POST" }),
catch: (e) => new PaymentError(`Refund failed: ${e}`)
}).pipe(Effect.asVoid)
}
// === Test реализация ===
const testPaymentGateway = {
charge: (amount: number, _cardToken: string) =>
Effect.succeed({
transactionId: "test-tx-123",
status: "success" as const,
amount
}),
refund: (_transactionId: string) => Effect.void
}
// === Failing test реализация ===
const failingPaymentGateway = {
charge: (_amount: number, _cardToken: string) =>
Effect.fail(new PaymentError("Card declined")),
refund: (_transactionId: string) =>
Effect.fail(new PaymentError("Transaction not found"))
}
// Тесты
const testSuccessfulPayment = processPayment("order-1", 100, "card-token").pipe(
Effect.provideService(PaymentGateway, testPaymentGateway)
)
const testFailedPayment = processPayment("order-2", 100, "bad-card").pipe(
Effect.provideService(PaymentGateway, failingPaymentGateway)
)
// Production
const productionPayment = processPayment("order-3", 100, "real-token").pipe(
Effect.provideService(PaymentGateway, productionPaymentGateway)
)
Пример 5: Вложенное предоставление сервисов
class Outer extends Context.Tag("Outer")<Outer, { value: string }>() {}
class Inner extends Context.Tag("Inner")<Inner, { value: string }>() {}
// Внешний эффект использует Outer
const outerEffect = Effect.gen(function* () {
const outer = yield* Outer
console.log(`Outer: ${outer.value}`)
// Внутренний эффект использует Inner
const innerEffect = Effect.gen(function* () {
const inner = yield* Inner
console.log(`Inner: ${inner.value}`)
return inner.value
})
// Предоставляем Inner внутри Outer
const innerResult = yield* innerEffect.pipe(
Effect.provideService(Inner, { value: `${outer.value}-inner` })
)
return `${outer.value} -> ${innerResult}`
})
// Предоставляем Outer снаружи
const runnable = outerEffect.pipe(
Effect.provideService(Outer, { value: "OUTER" })
)
Effect.runPromise(runnable).then(console.log)
// Output:
// Outer: OUTER
// Inner: OUTER-inner
// OUTER -> OUTER-inner
Пример 6: Условное предоставление сервисов
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
// Разные реализации логгера
const consoleLogger = {
log: (msg: string) => Effect.sync(() => console.log(`[CONSOLE] ${msg}`))
}
const jsonLogger = {
log: (msg: string) => Effect.sync(() =>
console.log(JSON.stringify({ timestamp: new Date().toISOString(), message: msg }))
)
}
const silentLogger = {
log: (_: string) => Effect.void
}
// Программа
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Starting application")
yield* logger.log("Processing...")
yield* logger.log("Done")
})
// Выбор логгера на основе конфигурации
const configuredProgram = Effect.gen(function* () {
const logFormat = yield* Config.string("LOG_FORMAT").pipe(
Config.withDefault("console")
)
const loggerImpl =
logFormat === "json" ? jsonLogger :
logFormat === "silent" ? silentLogger :
consoleLogger
return yield* program.pipe(
Effect.provideService(Logger, loggerImpl)
)
})
Effect.runPromise(configuredProgram)
Упражнения
Простое предоставление сервиса
Создайте и предоставьте сервис Greeter:
class Greeter extends Context.Tag("Greeter")<
Greeter,
{ readonly greet: (name: string) => Effect.Effect<string> }
>() {}
const program = Effect.gen(function* () {
const greeter = yield* Greeter
return yield* greeter.greet("World")
})
// Задание: предоставьте реализацию Greeter, которая возвращает "Hello, {name}!"
const runnable = // Ваш код здесь
// Тест:
Effect.runPromise(runnable).then(console.log)
// Ожидается: "Hello, World!"const runnable = program.pipe(
Effect.provideService(Greeter, {
greet: (name: string) => Effect.succeed(`Hello, ${name}!`)
})
)Множественные сервисы через Context
Создайте контекст с несколькими сервисами:
class Math extends Context.Tag("Math")<Math, {
readonly add: (a: number, b: number) => Effect.Effect<number>
readonly multiply: (a: number, b: number) => Effect.Effect<number>
}>() {}
class Formatter extends Context.Tag("Formatter")<Formatter, {
readonly format: (n: number) => Effect.Effect<string>
}>() {}
const program = Effect.gen(function* () {
const math = yield* Math
const formatter = yield* Formatter
const sum = yield* math.add(10, 20)
const product = yield* math.multiply(sum, 2)
return yield* formatter.format(product)
})
// Задание: создайте Context со обоими сервисами и предоставьте его
const ctx = // Ваш код здесь
const runnable = Effect.provide(program, ctx)
// Тест: должен вернуть "Result: 60"const ctx = Context.empty().pipe(
Context.add(Math, {
add: (a, b) => Effect.succeed(a + b),
multiply: (a, b) => Effect.succeed(a * b)
}),
Context.add(Formatter, {
format: (n) => Effect.succeed(`Result: ${n}`)
})
)Частичное предоставление
class A extends Context.Tag("A")<A, { value: number }>() {}
class B extends Context.Tag("B")<B, { value: string }>() {}
class C extends Context.Tag("C")<C, { value: boolean }>() {}
const program: Effect.Effect<string, never, A | B | C> = Effect.gen(function* () {
const a = yield* A
const b = yield* B
const c = yield* C
return `${a.value}-${b.value}-${c.value}`
})
// Задание: создайте три промежуточных эффекта, предоставляя по одному сервису
const withA = // Effect<string, never, B | C>
const withAB = // Effect<string, never, C>
const complete = // Effect<string, never, never>const withA = program.pipe(Effect.provideService(A, { value: 42 }))
// Тип: Effect<string, never, B | C>
const withAB = withA.pipe(Effect.provideService(B, { value: "hello" }))
// Тип: Effect<string, never, C>
const complete = withAB.pipe(Effect.provideService(C, { value: true }))
// Тип: Effect<string, never, never>provideServiceEffect
Создайте сервис, который требует асинхронной инициализации:
interface DatabaseConfig {
readonly host: string
readonly port: number
readonly database: string
}
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>, DatabaseError>
readonly close: () => Effect.Effect<void>
}
>() {}
// Задание: реализуйте createDatabase, который:
// 1. Читает конфигурацию из environment (используя Config)
// 2. "Подключается" к базе (просто выводит сообщение)
// 3. Возвращает реализацию Database
const createDatabase: Effect.Effect</* тип сервиса */, ConfigError, never> = // Ваш код
// Использование:
const program = Effect.gen(function* () {
const db = yield* Database
const users = yield* db.query("SELECT * FROM users")
return users
})
const runnable = program.pipe(
Effect.provideServiceEffect(Database, createDatabase)
)
const createDatabase: Effect.Effect<
Context.Tag.Service<typeof Database>,
ConfigError,
never
> = Effect.gen(function* () {
const host = yield* Config.string("DB_HOST")
const port = yield* Config.number("DB_PORT")
const database = yield* Config.string("DB_NAME")
console.log(`Connecting to ${host}:${port}/${database}`)
return {
query: (sql: string) => Effect.succeed([{ id: 1, name: "test" }]),
close: () => Effect.sync(() => console.log("Connection closed"))
}
})Переключение реализаций
Создайте систему, где реализация сервиса выбирается на основе условия:
class Storage extends Context.Tag("Storage")<
Storage,
{
readonly save: (key: string, value: string) => Effect.Effect<void>
readonly load: (key: string) => Effect.Effect<string | null>
}
>() {}
// Реализации
const memoryStorage: /* тип */ = // In-memory storage
const fileStorage: /* тип */ = // File-based storage (mock)
// Задание: создайте функцию, которая выбирает реализацию
const withStorage = (
useFile: boolean
): <A, E, R>(effect: Effect.Effect<A, E, R | Storage>) => Effect.Effect<A, E, R> => {
// Ваш код здесь
}
// Использование:
const program = Effect.gen(function* () {
const storage = yield* Storage
yield* storage.save("key", "value")
return yield* storage.load("key")
})
const inMemory = program.pipe(withStorage(false))
const inFile = program.pipe(withStorage(true))const memoryStorage: Context.Tag.Service<typeof Storage> = {
save: (key, value) => Effect.sync(() => {
memoryStore.set(key, value)
}),
load: (key) => Effect.succeed(memoryStore.get(key) ?? null)
}
const fileStorage: Context.Tag.Service<typeof Storage> = {
save: (key, value) => Effect.sync(() => {
console.log(`[FILE] Writing ${key}`)
}),
load: (key) => Effect.sync(() => {
console.log(`[FILE] Reading ${key}`)
return null
})
}
const withStorage = (
useFile: boolean
): <A, E, R>(effect: Effect.Effect<A, E, R | Storage>) => Effect.Effect<A, E, R> => {
return (effect) => effect.pipe(
Effect.provideService(Storage, useFile ? fileStorage : memoryStorage)
)
}Контекст-трансформер
Напишите функцию, которая трансформирует существующий контекст:
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
// Задание: создайте функцию, которая добавляет префикс ко всем сообщениям логгера
const withLogPrefix = <R>(
ctx: Context.Context<R | Logger>,
prefix: string
): Context.Context<R | Logger> => {
// Ваш код здесь
// Должна извлечь Logger из контекста, обернуть его, и вернуть новый контекст
}const withLogPrefix = <R>(
ctx: Context.Context<R | Logger>,
prefix: string
): Context.Context<R | Logger> => {
const logger = Context.get(ctx, Logger)
const prefixedLogger = {
log: (msg: string) => logger.log(`${prefix}${msg}`)
}
return ctx.pipe(
Context.omit(Logger),
Context.add(Logger, prefixedLogger)
) as Context.Context<R | Logger>
}Middleware-стиль предоставления
Реализуйте паттерн middleware для обогащения контекста:
// Request context
class RequestId extends Context.Tag("RequestId")<RequestId, { id: string }>() {}
class UserId extends Context.Tag("UserId")<UserId, { id: string | null }>() {}
class Timestamp extends Context.Tag("Timestamp")<Timestamp, { value: Date }>() {}
type RequestContext = RequestId | UserId | Timestamp
// Задание: создайте middleware, который:
// 1. Генерирует RequestId
// 2. Извлекает UserId из "заголовков" (мок)
// 3. Добавляет Timestamp
// 4. Выполняет эффект с этим контекстом
const withRequestContext = <A, E, R>(
effect: Effect.Effect<A, E, R | RequestContext>,
headers: Record<string, string>
): Effect.Effect<A, E, Exclude<R, RequestContext>> => {
// Ваш код здесь
}
// Использование:
const handler = Effect.gen(function* () {
const requestId = yield* RequestId
const userId = yield* UserId
const timestamp = yield* Timestamp
return {
requestId: requestId.id,
userId: userId.id,
processedAt: timestamp.value.toISOString()
}
})
const result = withRequestContext(handler, { "X-User-Id": "user-123" })const withRequestContext = <A, E, R>(
effect: Effect.Effect<A, E, R | RequestContext>,
headers: Record<string, string>
): Effect.Effect<A, E, Exclude<R, RequestContext>> =>
Effect.gen(function* () {
const requestId = crypto.randomUUID()
const userId = headers["X-User-Id"] ?? null
const timestamp = new Date()
return yield* effect.pipe(
Effect.provideService(RequestId, { id: requestId }),
Effect.provideService(UserId, { id: userId }),
Effect.provideService(Timestamp, { value: timestamp })
)
})Scoped предоставление
Реализуйте предоставление сервиса с автоматическим cleanup:
class Connection extends Context.Tag("Connection")<
Connection,
{
readonly execute: (query: string) => Effect.Effect<unknown>
readonly isConnected: () => Effect.Effect<boolean>
}
>() {}
// Задание: создайте функцию, которая:
// 1. Создаёт соединение
// 2. Предоставляет его в эффект
// 3. Автоматически закрывает соединение после выполнения
const withConnection = <A, E, R>(
effect: Effect.Effect<A, E, R | Connection>
): Effect.Effect<A, E | ConnectionError, Exclude<R, Connection>> => {
// Ваш код здесь
// Используйте Effect.acquireUseRelease или Effect.scoped
}const withConnection = <A, E, R>(
effect: Effect.Effect<A, E, R | Connection>
): Effect.Effect<A, E | ConnectionError, Exclude<R, Connection>> =>
Effect.acquireUseRelease(
// Acquire: создаём соединение
Effect.sync((): Connection => ({
execute: (query) => Effect.sync(() => console.log(`Executing: ${query}`)),
isConnected: () => Effect.succeed(true)
})),
// Use: выполняем эффект с соединением
(connection) => effect.pipe(
Effect.provideService(Connection, connection)
),
// Release: закрываем соединение
(connection) => Effect.sync(() => console.log("Connection closed"))
)Context inheritance
Реализуйте иерархию контекстов с наследованием:
// Базовый контекст приложения
class AppConfig extends Context.Tag("AppConfig")<AppConfig, { name: string; version: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
type BaseContext = AppConfig | Logger
// Контекст модуля наследует базовый и добавляет свои сервисы
class ModuleConfig extends Context.Tag("ModuleConfig")<ModuleConfig, { feature: string }>() {}
type ModuleContext = BaseContext | ModuleConfig
// Задание: создайте функцию для создания модульного контекста на основе базового
const createModuleContext = (
baseContext: Context.Context<BaseContext>,
moduleConfig: { feature: string }
): Context.Context<ModuleContext> => {
// Ваш код здесь
}
// И функцию для выполнения модульного эффекта
const runInModule = <A, E>(
baseContext: Context.Context<BaseContext>,
moduleConfig: { feature: string },
effect: Effect.Effect<A, E, ModuleContext>
): Effect.Effect<A, E, never> => {
// Ваш код здесь
}const createModuleContext = (
baseContext: Context.Context<BaseContext>,
moduleConfig: { feature: string }
): Context.Context<ModuleContext> => {
return Context.merge(
baseContext,
Context.make(ModuleConfig, moduleConfig)
) as Context.Context<ModuleContext>
}
const runInModule = <A, E>(
baseContext: Context.Context<BaseContext>,
moduleConfig: { feature: string },
effect: Effect.Effect<A, E, ModuleContext>
): Effect.Effect<A, E, never> => {
const fullContext = createModuleContext(baseContext, moduleConfig)
return Effect.provide(effect, fullContext)
}Заключение
В этой статье мы изучили:
- 📖 Effect.provideService — базовый механизм для предоставления одного сервиса
- 📖 Effect.provide — универсальная функция для Context, Layer, Runtime
- 📖 Effect.provideServiceEffect — динамическое создание сервисов
- 📖 Context — контейнер для объединения нескольких сервисов
- 📖 Частичное предоставление — поэтапное удовлетворение зависимостей
💡 Ключевой takeaway: provideService — это “мост” между описанием программы и её выполнением. TypeScript гарантирует, что все зависимости удовлетворены до запуска.