Effect Курс Создание Layer

Создание Layer

Полное руководство по конструкторам Layer.

Теория

Выбор правильного конструктора

Каждый конструктор Layer предназначен для конкретного сценария. Выбор определяется ответами на три вопроса:

Вопрос 1: Нужны ли побочные эффекты при создании сервиса?
  Нет  → Layer.succeed / Layer.sync
  Да   → Вопрос 2

Вопрос 2: Нужно ли управлять жизненным циклом (acquire/release)?
  Нет  → Layer.effect
  Да   → Layer.scoped

Вопрос 3: Нужен ли доступ к зависимостям?
  Нет  → Layer.succeed / Layer.sync (для простых)
  Да   → Layer.effect / Layer.scoped (yield* Tag внутри)

Диаграмма принятия решения:

                  ┌──────────────────────┐
                  │ Создание сервиса     │
                  └──────────┬───────────┘

                  ┌──────────▼───────────┐
                  │ Есть побочные        │
                  │ эффекты?             │
                  └──┬──────────────┬────┘
                  Нет│              │Да
                     ▼              ▼
              ┌─────────────┐ ┌─────────────────┐
              │ Значение    │ │ Нужен            │
              │ статичное?  │ │ lifecycle?       │
              └──┬──────┬───┘ └──┬──────────┬───┘
              Да │   Нет│     Нет│          │Да
                 ▼      ▼        ▼          ▼
           succeed    sync    effect     scoped

Конструкторы Layer

Layer.succeed

Layer.succeed — самый простой конструктор. Создаёт Layer из готового значения без каких-либо эффектов.

Сигнатура

function succeed<T extends Context.Tag<any, any>>(
  tag: T,
  service: Context.Tag.Service<T>
): Layer<Context.Tag.Identifier<T>>

Когда использовать

  • Сервис не требует инициализации
  • Значение известно на этапе определения
  • Нет зависимостей от других сервисов
  • Нет побочных эффектов

Примеры


// === Простая конфигурация ===

class AppConfig extends Context.Tag("AppConfig")<
  AppConfig,
  {
    readonly port: number
    readonly host: string
    readonly env: "development" | "production" | "test"
  }
>() {}

const AppConfigLive = Layer.succeed(AppConfig, {
  port: 3000,
  host: "0.0.0.0",
  env: "production"
})
// Тип: Layer<AppConfig, never, never>

// === Сервис с методами, возвращающими Effect ===

class MathService extends Context.Tag("MathService")<
  MathService,
  {
    readonly add: (a: number, b: number) => Effect.Effect<number>
    readonly multiply: (a: number, b: number) => Effect.Effect<number>
  }
>() {}

const MathServiceLive = Layer.succeed(MathService, {
  add: (a, b) => Effect.succeed(a + b),
  multiply: (a, b) => Effect.succeed(a * b)
})
// Тип: Layer<MathService, never, never>

⚠️ Важно: Layer.succeed захватывает значение по ссылке. Если значение содержит мутабельное состояние, все потребители получат одну и ту же ссылку. Это поведение по дизайну, но требует осознанности:

// ⚠️ Все потребители разделяют один Map
const CacheLive = Layer.succeed(Cache, {
  store: new Map<string, string>(),
  get: (key) => Effect.sync(() => /* ... */ undefined),
  set: (key, value) => Effect.sync(() => { /* ... */ })
})

Layer.sync

Layer.sync создаёт Layer из синхронной функции. Функция вызывается при конструировании Layer (лениво).

Сигнатура

function sync<T extends Context.Tag<any, any>>(
  tag: T,
  service: () => Context.Tag.Service<T>
): Layer<Context.Tag.Identifier<T>>

Когда использовать

  • Значение нужно вычислить при инициализации (но без побочных эффектов)
  • Создание значения требует вычислений (чтение из памяти, парсинг)
  • Нужна ленивая инициализация

Примеры


class Clock extends Context.Tag("Clock")<
  Clock,
  {
    readonly now: () => Effect.Effect<number>
    readonly startedAt: number
  }
>() {}

// Значение startedAt вычисляется при конструировании Layer
const ClockLive = Layer.sync(Clock, () => ({
  now: () => Effect.sync(() => Date.now()),
  startedAt: Date.now()
}))

// === Конфиг из переменных окружения (синхронно) ===

class EnvConfig extends Context.Tag("EnvConfig")<
  EnvConfig,
  {
    readonly nodeEnv: string
    readonly debug: boolean
  }
>() {}

const EnvConfigLive = Layer.sync(EnvConfig, () => ({
  nodeEnv: process.env["NODE_ENV"] ?? "development",
  debug: process.env["DEBUG"] === "true"
}))

Отличие от Layer.succeed

// Layer.succeed — значение вычисляется СРАЗУ при определении
const a = Layer.succeed(Tag, { timestamp: Date.now() })
// timestamp зафиксировано в момент определения

// Layer.sync — функция вызывается ЛЕНИВО при конструировании
const b = Layer.sync(Tag, () => ({ timestamp: Date.now() }))
// timestamp вычисляется при первом использовании Layer

Layer.fail и Layer.failSync

Создают Layer, который всегда завершается ошибкой. Полезны для тестирования и fallback-стратегий.

Сигнатура

function fail<E>(error: E): Layer<never, E>
function failSync<E>(error: () => E): Layer<never, E>

Примеры


class DatabaseError extends Data.TaggedError("DatabaseError")<{
  readonly reason: string
}> {}

// Layer, который всегда падает — полезен для тестирования error paths
const DatabaseFailing = Layer.fail(
  new DatabaseError({ reason: "Connection refused" })
)
// Тип: Layer<never, DatabaseError, never>

// Ленивая версия
const DatabaseFailingLazy = Layer.failSync(() =>
  new DatabaseError({ reason: `Failed at ${new Date().toISOString()}` })
)

Layer.effect

Layer.effect — основной конструктор для создания Layer из Effect. Это самый универсальный конструктор, позволяющий использовать зависимости, выполнять побочные эффекты и обрабатывать ошибки.

Сигнатура

function effect<T extends Context.Tag<any, any>, E, R>(
  tag: T,
  effect: Effect<Context.Tag.Service<T>, E, R>
): Layer<Context.Tag.Identifier<T>, E, R>

Когда использовать

  • Сервис зависит от других сервисов (через yield* Tag)
  • Инициализация включает асинхронные операции
  • Конструирование может завершиться ошибкой
  • Нужно выполнить побочные эффекты при создании

Примеры


// === Сервис с зависимостями ===

class Config extends Context.Tag("Config")<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>() {}

class Logger extends Context.Tag("Logger")<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>() {}

// ConfigLive — без зависимостей
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "postgres://localhost:5432/app"
  })
})

// LoggerLive — зависит от Config
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    // yield* Config — доступ к зависимости
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        })
    }
  })
)
// Тип: Layer<Logger, never, Config>

// === Сервис с ошибками при конструировании ===

class ConnectionError extends Data.TaggedError("ConnectionError")<{
  readonly url: string
  readonly cause: string
}> {}

class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>() {}

const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    const { connection } = yield* config.getConfig

    // Симуляция проверки подключения
    yield* logger.log(`Connecting to database: ${connection}`)
    
    if (connection.includes("invalid")) {
      return yield* Effect.fail(
        new ConnectionError({ url: connection, cause: "Invalid connection string" })
      )
    }

    yield* logger.log("Database connected successfully")

    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing: ${sql}`)
          return { result: `Results from ${connection}` }
        })
    }
  })
)
// Тип: Layer<Database, ConnectionError, Config | Logger>

Паттерн: Layer.effect с Effect.acquireRelease (без Scope)

Иногда нужно создать ресурс в Layer.effect, но без полного lifecycle. В таком случае ресурс будет жить столько же, сколько Scope программы:


class HttpClient extends Context.Tag("HttpClient")<
  HttpClient,
  { readonly fetch: (url: string) => Effect.Effect<string> }
>() {}

// Простой клиент без управления lifecycle
const HttpClientLive = Layer.effect(
  HttpClient,
  Effect.gen(function* () {
    yield* Effect.log("HttpClient initialized")
    return {
      fetch: (url) => Effect.succeed(`Response from ${url}`)
    }
  })
)

Layer.scoped

Layer.scoped — конструктор для создания Layer из scoped Effect. Ключевое отличие от Layer.effect — поддержка acquireRelease для управления жизненным циклом ресурсов.

Сигнатура

function scoped<T extends Context.Tag<any, any>, E, R>(
  tag: T,
  effect: Effect<Context.Tag.Service<T>, E, R>
): Layer<Context.Tag.Identifier<T>, E, Exclude<R, Scope>>

⚠️ Обратите внимание: Scope автоматически исключается из RIn. Layer сам управляет Scope.

Когда использовать

  • Сервис оборачивает ресурс с жизненным циклом (connection pool, file handle, socket)
  • Нужна гарантия освобождения ресурса при завершении программы
  • Используется Effect.acquireRelease или Effect.addFinalizer

Примеры


// === Connection Pool ===

class ConnectionPool extends Context.Tag("ConnectionPool")<
  ConnectionPool,
  {
    readonly getConnection: () => Effect.Effect<{ readonly id: string }>
    readonly size: number
  }
>() {}

const ConnectionPoolLive = Layer.scoped(
  ConnectionPool,
  Effect.gen(function* () {
    // Acquire: создаём пул
    const pool = yield* Effect.acquireRelease(
      Effect.sync(() => {
        console.log("[Pool] Creating connection pool (size: 10)")
        return {
          connections: Array.from({ length: 10 }, (_, i) => ({
            id: `conn-${i}`,
            inUse: false
          }))
        }
      }),
      (pool) =>
        Effect.sync(() => {
          console.log(`[Pool] Closing all ${pool.connections.length} connections`)
          pool.connections.forEach((c) => {
            console.log(`[Pool] Closed ${c.id}`)
          })
        })
    )

    let nextIdx = 0

    return {
      getConnection: () =>
        Effect.sync(() => {
          const conn = pool.connections[nextIdx % pool.connections.length]!
          nextIdx++
          return { id: conn.id }
        }),
      size: pool.connections.length
    }
  })
)
// Тип: Layer<ConnectionPool, never, never>
// Scope НЕ появляется в RIn — Layer управляет им сам

// === Пример использования ===

const program = Effect.gen(function* () {
  const pool = yield* ConnectionPool
  console.log(`Pool size: ${pool.size}`)
  const conn = yield* pool.getConnection()
  console.log(`Got connection: ${conn.id}`)
})

Effect.runPromise(Effect.provide(program, ConnectionPoolLive))
// Output:
// [Pool] Creating connection pool (size: 10)
// Pool size: 10
// Got connection: conn-0
// [Pool] Closing all 10 connections
// [Pool] Closed conn-0
// ... (все 10)

Layer.scoped с addFinalizer

Альтернативный способ — добавить финализатор без acquireRelease:


class TempDirectory extends Context.Tag("TempDirectory")<
  TempDirectory,
  { readonly path: string }
>() {}

const TempDirectoryLive = Layer.scoped(
  TempDirectory,
  Effect.gen(function* () {
    const path = `/tmp/app-${Date.now()}`
    console.log(`[TempDir] Creating: ${path}`)

    // Финализатор — будет вызван при закрытии Scope
    yield* Effect.addFinalizer(() =>
      Effect.sync(() => {
        console.log(`[TempDir] Removing: ${path}`)
      })
    )

    return { path }
  })
)

Порядок финализации

Финализаторы выполняются в обратном порядке — LIFO (Last In, First Out):

Создание:    Config → Logger → Database → Cache
Финализация: Cache → Database → Logger → Config

Layer.function

Layer.function создаёт Layer, который преобразует один сервис в другой через чистую функцию.

Сигнатура

function function_<A extends Context.Tag<any, any>, B extends Context.Tag<any, any>>(
  tagA: A,
  tagB: B,
  f: (a: Context.Tag.Service<A>) => Context.Tag.Service<B>
): Layer<Context.Tag.Identifier<B>, never, Context.Tag.Identifier<A>>

Когда использовать

  • Нужно адаптировать один сервис к интерфейсу другого
  • Преобразование чистое (без побочных эффектов)
  • Паттерн Adapter

Примеры


// Внутренний сервис с одним интерфейсом
class InternalLogger extends Context.Tag("InternalLogger")<
  InternalLogger,
  {
    readonly write: (level: string, message: string) => Effect.Effect<void>
  }
>() {}

// Публичный интерфейс логгера
class AppLogger extends Context.Tag("AppLogger")<
  AppLogger,
  {
    readonly info: (message: string) => Effect.Effect<void>
    readonly error: (message: string) => Effect.Effect<void>
    readonly debug: (message: string) => Effect.Effect<void>
  }
>() {}

// Адаптируем InternalLogger к AppLogger
const AppLoggerFromInternal = Layer.function(InternalLogger, AppLogger, (internal) => ({
  info: (msg) => internal.write("INFO", msg),
  error: (msg) => internal.write("ERROR", msg),
  debug: (msg) => internal.write("DEBUG", msg)
}))
// Тип: Layer<AppLogger, never, InternalLogger>

// Можно скомпоновать с реальным InternalLogger
const InternalLoggerLive = Layer.succeed(InternalLogger, {
  write: (level, message) => Effect.log(`[${level}] ${message}`)
})

const AppLoggerLive = AppLoggerFromInternal.pipe(
  Layer.provide(InternalLoggerLive)
)
// Тип: Layer<AppLogger, never, never>

Layer.context

Layer.context создаёт Layer, который запрашивает контекст R и предоставляет его как есть. По сути, это identity Layer.

Сигнатура

function context<R>(): Layer<R, never, R>

Когда использовать

  • Нужно явно передать зависимости «как есть»
  • Компоновка с другими Layer, где часть зависимостей просто пробрасывается

Пример


class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => void }>() {}

// Identity layer — просто пробрасывает Config
const passthrough = Layer.context<Config>()
// Тип: Layer<Config, never, Config>

// Полезно при merge: собираем Layer, который пробрасывает Config и добавляет Logger
const combined = Layer.merge(
  passthrough,
  Layer.succeed(Logger, { log: console.log })
)
// Тип: Layer<Config | Logger, never, Config>

Layer.succeedContext

Layer.succeedContext создаёт Layer из готового объекта Context.

Сигнатура

function succeedContext<R>(context: Context.Context<R>): Layer<R>

Пример


class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => void }>() {}

// Создаём Context вручную
const ctx = Context.empty().pipe(
  Context.add(Config, { port: 8080 }),
  Context.add(Logger, { log: console.log })
)

// Layer из готового контекста
const AppLayer = Layer.succeedContext(ctx)
// Тип: Layer<Config | Logger, never, never>

Effect.Service API

Начиная с Effect 3.x, существует упрощённый способ определения сервисов — Effect.Service. Он объединяет определение Tag, реализацию и Layer в одном месте.

Базовый синтаксис


class MyService extends Effect.Service<MyService>()("MyService", {
  // Определение реализации (одно из):
  succeed: { /* статическое значение */ },
  // ИЛИ
  sync: () => { /* синхронная инициализация */ },
  // ИЛИ
  effect: Effect.gen(function* () { /* эффектная инициализация */ }),
  // ИЛИ
  scoped: Effect.gen(function* () { /* scoped инициализация */ }),

  // Опционально: зависимости
  dependencies: [OtherService.Default]
}) {}

Генерируемые Layer

LayerОписание
MyService.DefaultLayer с включёнными зависимостями
MyService.DefaultWithoutDependenciesLayer без зависимостей (они должны быть предоставлены отдельно)

Полный пример


// === Config Service ===

class Config extends Effect.Service<Config>()("Config", {
  succeed: {
    port: 3000,
    host: "localhost",
    dbUrl: "postgres://localhost:5432/app"
  }
}) {}

// === Logger Service ===

class Logger extends Effect.Service<Logger>()("Logger", {
  effect: Effect.gen(function* () {
    const config = yield* Config
    return {
      info: (msg: string) => Effect.log(`[${config.host}] INFO: ${msg}`),
      error: (msg: string) => Effect.log(`[${config.host}] ERROR: ${msg}`)
    }
  }),
  dependencies: [Config.Default]
}) {}

// === Database Service ===

class Database extends Effect.Service<Database>()("Database", {
  scoped: Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger

    const connection = yield* Effect.acquireRelease(
      Effect.gen(function* () {
        yield* logger.info(`Connecting to ${config.dbUrl}`)
        return { url: config.dbUrl, connected: true as const }
      }),
      (conn) =>
        Effect.gen(function* () {
          yield* logger.info(`Disconnecting from ${conn.url}`)
        })
    )

    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.info(`SQL: ${sql}`)
          return { rows: [], connection: connection.url }
        })
    }
  }),
  dependencies: [Config.Default, Logger.Default]
}) {}

// === Использование ===

const program = Effect.gen(function* () {
  const db = yield* Database
  const result = yield* db.query("SELECT 1")
  return result
})

// Database.Default уже содержит все зависимости!
Effect.runPromise(Effect.provide(program, Database.Default)).then(console.log)

Мокирование через Effect.Service

// Мок через конструктор
const dbMock = new Database({
  query: (sql) => Effect.succeed({ rows: [{ mock: true }], connection: "mock" })
})

const testProgram = program.pipe(
  Effect.provideService(Database, dbMock)
)

Layer.launch

Layer.launch конвертирует Layer в Effect, конструирует его и держит alive до прерывания. Идеально подходит для серверов.

Сигнатура

function launch<ROut, E, RIn>(
  self: Layer<ROut, E, RIn>
): Effect<never, E, RIn>

Пример


class HttpServer extends Context.Tag("HttpServer")<HttpServer, void>() {}

const HttpServerLive = Layer.scoped(
  HttpServer,
  Effect.gen(function* () {
    yield* Effect.acquireRelease(
      Effect.sync(() => console.log("Server started on :3000")),
      () => Effect.sync(() => console.log("Server stopped"))
    )
    yield* Effect.addFinalizer(() =>
      Console.log("Cleaning up server resources...")
    )
  })
)

// launch конструирует Layer и держит его пока не прервут
Effect.runFork(Layer.launch(HttpServerLive))
// Output: Server started on :3000
// (процесс не завершается, ждёт прерывания)

Layer.tap и Layer.tapError

Позволяют выполнять побочные эффекты при успешном или неуспешном конструировании Layer без изменения его типа.

Сигнатуры

function tap<ROut, XR>(
  self: Layer<ROut, E, RIn>,
  f: (context: Context.Context<ROut>) => Effect<any, never, XR>
): Layer<ROut, E, RIn | XR>

function tapError<E, XE, XR>(
  self: Layer<ROut, E, RIn>,
  f: (error: E) => Effect<any, XE, XR>
): Layer<ROut, E | XE, RIn | XR>

Пример


class HttpServer extends Context.Tag("HttpServer")<HttpServer, void>() {}

const HttpServerLive = Layer.effect(
  HttpServer,
  Effect.gen(function* () {
    const host = yield* Config.string("HOST")
    console.log(`Listening on ${host}`)
  })
).pipe(
  Layer.tap((_ctx) =>
    Console.log("Layer construction succeeded")
  ),
  Layer.tapError((error) =>
    Console.log(`Layer construction failed: ${error}`)
  )
)

Обработка ошибок: catchAll, orElse

Layer.catchAll

Перехватывает ошибку конструирования и возвращает fallback Layer:


class Database extends Context.Tag("Database")<
  Database,
  { readonly type: string }
>() {}

class DbError extends Data.TaggedError("DbError")<{
  readonly message: string
}> {}

const PostgresLayer: Layer.Layer<Database, DbError> = Layer.effect(
  Database,
  Effect.gen(function* () {
    return yield* Effect.fail(new DbError({ message: "Connection refused" }))
  })
)

const SqliteLayer: Layer.Layer<Database> = Layer.succeed(Database, {
  type: "sqlite"
})

// При ошибке PostgresLayer — переключаемся на SQLite
const DatabaseLive = PostgresLayer.pipe(
  Layer.catchAll((error) => {
    console.log(`Postgres failed: ${error.message}, falling back to SQLite`)
    return SqliteLayer
  })
)
// Тип: Layer<Database, never, never>

Layer.orElse

Упрощённый fallback без доступа к ошибке:

const DatabaseLive2 = PostgresLayer.pipe(
  Layer.orElse(() => SqliteLayer)
)

Layer.retry

Повторяет попытку конструирования Layer по заданному Schedule:


class ExternalApi extends Context.Tag("ExternalApi")<
  ExternalApi,
  { readonly call: (endpoint: string) => Effect.Effect<unknown> }
>() {}

class ApiError extends Data.TaggedError("ApiError")<{
  readonly status: number
}> {}

const ExternalApiLive = Layer.effect(
  ExternalApi,
  Effect.gen(function* () {
    yield* Effect.log("Attempting to connect to external API...")
    // Симуляция подключения
    return {
      call: (endpoint: string) => Effect.succeed({ data: endpoint })
    }
  })
).pipe(
  // Повторять до 3 раз с экспоненциальной задержкой
  Layer.retry(Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(3))))
)

Сравнение конструкторов

КонструкторЗависимостиЭффектыLifecycleТип
Layer.succeedLayer<A>
Layer.syncСинхронныеLayer<A>
Layer.failLayer<never, E>
Layer.effectLayer<A, E, R>
Layer.scopedLayer<A, E, R>
Layer.function✅ (один)Layer<B, never, A>
Layer.context✅ (pass)Layer<R, never, R>
Layer.succeedContextLayer<R>
Effect.ServiceГенерирует Layer автоматически

Примеры

Пример: Production-ready сервисы


// === Typed Errors ===

class ConfigError extends Data.TaggedError("ConfigError")<{
  readonly key: string
  readonly message: string
}> {}

class RedisError extends Data.TaggedError("RedisError")<{
  readonly command: string
  readonly cause: string
}> {}

// === Services ===

class Config extends Context.Tag("Config")<
  Config,
  {
    readonly get: (key: string) => Effect.Effect<string, ConfigError>
  }
>() {}

class Redis extends Context.Tag("Redis")<
  Redis,
  {
    readonly get: (key: string) => Effect.Effect<string | null, RedisError>
    readonly set: (key: string, value: string, ttl?: number) => Effect.Effect<void, RedisError>
  }
>() {}

class Cache extends Context.Tag("Cache")<
  Cache,
  {
    readonly lookup: <A>(
      key: string,
      compute: Effect.Effect<A>
    ) => Effect.Effect<A, RedisError>
  }
>() {}

// === Layer Implementations ===

// Config из environment
const ConfigLive = Layer.sync(Config, () => ({
  get: (key: string) => {
    const value = process.env[key]
    return value !== undefined
      ? Effect.succeed(value)
      : Effect.fail(new ConfigError({ key, message: `${key} not found in environment` }))
  }
}))

// Redis с lifecycle
const RedisLive = Layer.scoped(
  Redis,
  Effect.gen(function* () {
    const config = yield* Config
    const url = yield* config.get("REDIS_URL").pipe(
      Effect.catchAll(() => Effect.succeed("redis://localhost:6379"))
    )

    const client = yield* Effect.acquireRelease(
      Effect.sync(() => {
        console.log(`[Redis] Connecting to ${url}`)
        return { url, store: new Map<string, string>() }
      }),
      (client) =>
        Effect.sync(() => {
          console.log(`[Redis] Disconnecting from ${client.url}`)
          client.store.clear()
        })
    )

    return {
      get: (key) =>
        Effect.sync(() => client.store.get(key) ?? null),
      set: (key, value, _ttl) =>
        Effect.sync(() => { client.store.set(key, value) })
    }
  })
)
// Тип: Layer<Redis, never, Config>

// Cache с паттерном cache-aside
const CacheLive = Layer.effect(
  Cache,
  Effect.gen(function* () {
    const redis = yield* Redis
    return {
      lookup: <A>(key: string, compute: Effect.Effect<A>) =>
        Effect.gen(function* () {
          const cached = yield* redis.get(key)
          if (cached !== null) {
            return JSON.parse(cached) as A
          }
          const value = yield* compute
          yield* redis.set(key, JSON.stringify(value))
          return value
        })
    }
  })
)
// Тип: Layer<Cache, never, Redis>

Упражнения

Упражнение

Выбор правильного конструктора

Легко

Для каждого сценария определите, какой конструктор Layer использовать, и реализуйте:

// 1. Сервис генерации UUID (без зависимостей, без побочных эффектов)
// 2. Сервис текущего времени (значение вычисляется при инициализации)
// 3. Сервис форматирования (зависит от Config, нет lifecycle)
// 4. HTTP-клиент (зависит от Config, имеет lifecycle — open/close)
Упражнение

Effect.Service с тестированием

Средне

Реализуйте NotificationService через Effect.Service с двумя стратегиями отправки:

// NotificationService с методами:
//   send(to: string, message: string): Effect.Effect<void, NotificationError>
//   sendBatch(messages: ReadonlyArray<{to: string, message: string}>): Effect.Effect<number, NotificationError>
//
// Зависимости: Logger, Config
// 
// Требования:
// 1. Реализуйте через Effect.Service
// 2. Создайте тестовый мок через конструктор
// 3. Напишите программу, которая отправляет 3 уведомления
Упражнение

Layer.scoped с graceful shutdown

Сложно

Создайте WebSocket-сервер как Layer с полным lifecycle:

// WebSocketServer с:
// - Инициализация: открытие порта, логирование
// - Финализация: закрытие всех соединений, остановка сервера
// - Зависимости: Config, Logger
// - Ошибка конструирования: PortInUseError
//
// Требования:
// 1. Используйте Layer.scoped
// 2. acquireRelease для сервера
// 3. addFinalizer для дополнительной очистки
// 4. Layer.retry при PortInUseError (3 попытки)
// 5. Layer.tap для логирования успеха конструирования

🔗 Далее: Композиция Layer: provide, provideMerge — как собирать Layer в единый граф зависимостей