Effect Курс Предоставление сервисов

Предоставление сервисов

Как предоставлять сервисы в 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:

  1. Создаёт новый Context с предоставленным сервисом
  2. Выполняет эффект в этом контексте
  3. Убирает требование сервиса из типа 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!"
Упражнение

Множественные сервисы через 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"
Упражнение

Частичное предоставление

Легко

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

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

Переключение реализаций

Средне

Создайте систему, где реализация сервиса выбирается на основе условия:


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

Контекст-трансформер

Средне

Напишите функцию, которая трансформирует существующий контекст:


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 из контекста, обернуть его, и вернуть новый контекст
}
Упражнение

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

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

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> => {
  // Ваш код здесь
}

Заключение

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

  • 📖 Effect.provideService — базовый механизм для предоставления одного сервиса
  • 📖 Effect.provide — универсальная функция для Context, Layer, Runtime
  • 📖 Effect.provideServiceEffect — динамическое создание сервисов
  • 📖 Context — контейнер для объединения нескольких сервисов
  • 📖 Частичное предоставление — поэтапное удовлетворение зависимостей

💡 Ключевой takeaway: provideService — это “мост” между описанием программы и её выполнением. TypeScript гарантирует, что все зависимости удовлетворены до запуска.