Effect Курс Композиция Layer

Композиция Layer

Вертикальная композиция Layer.

Теория

Что такое композиция Layer

Композиция Layer — это процесс соединения Layer друг с другом для разрешения зависимостей. Если Layer A производит сервис, который нужен Layer B, мы можем скомпоновать их, чтобы получить Layer, который не требует этот промежуточный сервис.

Существует два основных вида композиции:

Вертикальная (provide):
  Выход одного Layer подаётся на вход другого

  Layer<Logger, never, Config>    +    Layer<Config>
  ────────────────────────────────────────────────
  Layer<Logger>                   (Config разрешён)

Горизонтальная (merge):
  Два Layer объединяются в один параллельный

  Layer<Config>    +    Layer<Logger, never, Config>
  ──────────────────────────────────────────────────
  Layer<Config | Logger, never, Config>   (оба выхода)

В этой статье мы сфокусируемся на вертикальной композиции через provide и provideMerge. Горизонтальная композиция (merge, mergeAll) рассматривается в следующей статье.

Визуальная модель provide

      BEFORE provide                    AFTER provide
      ─────────────                     ──────────────

  ┌────────────────────┐          ┌────────────────────┐
  │    LoggerLive       │          │    Result           │
  │                    │          │                    │
  │ In: Config         │          │ In: (nothing)      │
  │ Out: Logger        │          │ Out: Logger        │
  └────────┬───────────┘          └────────────────────┘
           │ needs Config

  ┌────────▼───────────┐
  │    ConfigLive       │
  │                    │
  │ In: (nothing)      │
  │ Out: Config        │
  └────────────────────┘

  Layer.provide(LoggerLive, ConfigLive) → Layer<Logger>

provide «подключает» выход ConfigLive (Config) ко входу LoggerLive (Config), устраняя эту зависимость из результирующего типа.


Концепция ФП

Композиция как морфизмов

В категории контекстов Layer представляет морфизм. Layer.provide реализует композицию морфизмов:

f : B → A    (LoggerLive: Config → Logger)
g : C → B    (ConfigLive: never → Config)
──────────────────────────────────────────
f ∘ g : C → A   (result: never → Logger)

Это в точности композиция функций, но на уровне контекстов. Порядок аргументов в Layer.provide соответствует pipe-стилю: “предоставь inner слою зависимости из outer”:

Layer.provide(inner, outer)
//            ↑       ↑
//          кому    откуда

Частичная аппликация

Layer.provide поддерживает частичное предоставление зависимостей. Если Layer требует A | B, а мы предоставляем только A, то результирующий Layer всё ещё требует B:

Layer<Out, E, A | B>  +  Layer<A>
──────────────────────────────────
Layer<Out, E, B>     (A разрешён, B остаётся)

Это аналог частичной аппликации функций в ФП.


Layer.provide

Сигнатура

function provide<ROut, E, RIn, ROut2, E2, RIn2>(
  self: Layer<ROut, E, RIn>,
  that: Layer<ROut2, E2, RIn2>
): Layer<ROut, E | E2, RIn2 | Exclude<RIn, ROut2>>

Разбор типов:

self:   Layer<ROut, E, RIn>        — "внутренний" Layer (потребитель)
that:   Layer<ROut2, E2, RIn2>     — "внешний" Layer (поставщик)

Результат:
  ROut  = ROut (тот же выход, что у внутреннего)
  E     = E | E2 (ошибки объединяются)
  RIn   = RIn2 | Exclude<RIn, ROut2> (зависимости внешнего + неразрешённые внутреннего)

Базовые примеры


class Config extends Context.Tag("Config")<
  Config,
  { readonly port: number; readonly host: string }
>() {}

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

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

// Layer<Config>
const ConfigLive = Layer.succeed(Config, {
  port: 3000,
  host: "localhost"
})

// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (msg) => Effect.log(`[${config.host}:${config.port}] ${msg}`)
    }
  })
)

// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql) =>
        Effect.gen(function* () {
          yield* logger.log(`SQL: ${sql}`)
          return { rows: [] }
        })
    }
  })
)

// === Пошаговая композиция ===

// Шаг 1: Предоставить Config для Logger
const LoggerResolved = Layer.provide(LoggerLive, ConfigLive)
// Тип: Layer<Logger, never, never>
// Config разрешён!

// Шаг 2: Предоставить Config и Logger для Database
const DatabaseResolved = DatabaseLive.pipe(
  Layer.provide(LoggerResolved),  // Logger → resolved
  Layer.provide(ConfigLive)       // Config → resolved
)
// Тип: Layer<Database, never, never>

Pipe-стиль

Layer.provide отлично работает в pipe:

// Цепочка provide в pipe
const DatabaseResolved2 = DatabaseLive.pipe(
  Layer.provide(LoggerLive),   // Logger ещё нуждается в Config
  Layer.provide(ConfigLive)    // Config разрешает и Logger, и Database
)
// Тип: Layer<Database, never, never>

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


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

// Сервис зависит от Config, Logger И Metrics
class App extends Context.Tag("App")<
  App,
  { readonly run: () => Effect.Effect<void> }
>() {}

const AppLive: Layer.Layer<App, never, Config | Logger | Metrics> = Layer.effect(
  App,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    const metrics = yield* Metrics
    return {
      run: () => Effect.gen(function* () {
        logger.log(`Starting on port ${config.port}`)
        metrics.track("app.started")
      })
    }
  })
)

// Предоставляем только Config
const step1 = Layer.provide(AppLive, Layer.succeed(Config, { port: 3000 }))
// Тип: Layer<App, never, Logger | Metrics>
//                         ↑ Config убран, Logger и Metrics остаются

// Предоставляем Logger
const step2 = Layer.provide(step1, Layer.succeed(Logger, { log: console.log }))
// Тип: Layer<App, never, Metrics>
//                         ↑ Осталась только Metrics

// Предоставляем Metrics
const step3 = Layer.provide(step2, Layer.succeed(Metrics, { track: console.log }))
// Тип: Layer<App, never, never>
//                         ↑ Все зависимости разрешены!

Объединение ошибок

При provide ошибки обоих Layer объединяются через union:


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

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

const ConfigLive: Layer.Layer<Config, ConfigError> = Layer.effect(
  Config,
  Effect.gen(function* () {
    const url = process.env["DB_URL"]
    if (!url) return yield* Effect.fail(new ConfigError({ key: "DB_URL" }))
    return { url }
  })
)

const DatabaseLive: Layer.Layer<Database, DbError, Config> = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      query: (sql) => Effect.succeed({ result: sql })
    }
  })
)

const resolved = Layer.provide(DatabaseLive, ConfigLive)
// Тип: Layer<Database, ConfigError | DbError, never>
// Ошибки ОБОИХ Layer объединены!

Layer.provideMerge

Сигнатура

function provideMerge<ROut, E, RIn, ROut2, E2, RIn2>(
  self: Layer<ROut, E, RIn>,
  that: Layer<ROut2, E2, RIn2>
): Layer<ROut | ROut2, E | E2, RIn2 | Exclude<RIn, ROut2>>

Ключевое отличие от provide

Layer.provide оставляет в выходе только сервисы внутреннего Layer. Layer.provideMerge добавляет в выход также сервисы внешнего Layer:

Layer.provide:
  LoggerLive.pipe(Layer.provide(ConfigLive))
  → Layer<Logger>                              (только Logger!)

Layer.provideMerge:
  LoggerLive.pipe(Layer.provideMerge(ConfigLive))
  → Layer<Logger | Config>                     (Logger + Config!)

Визуально:

      provide                         provideMerge
      ───────                         ────────────

  ┌──────────┐                    ┌──────────┐
  │  Logger  │ ← только Logger    │  Logger  │
  └──────────┘   на выходе        │  Config  │ ← оба на выходе
                                   └──────────┘

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

provideMerge необходим, когда вы хотите, чтобы промежуточные зависимости оставались доступны программе. Типичный сценарий — программа использует и Config, и Database:


class Config extends Context.Tag("Config")<
  Config,
  { readonly dbUrl: string; readonly logLevel: string }
>() {}

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

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

const ConfigLive = Layer.succeed(Config, {
  dbUrl: "postgres://localhost/app",
  logLevel: "INFO"
})

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (msg) => Effect.log(`[${config.logLevel}] ${msg}`)
    }
  })
)

const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing: ${sql}`)
          return { url: config.dbUrl, sql }
        })
    }
  })
)

// === С provide: Config теряется ===

const AppWithProvide = DatabaseLive.pipe(
  Layer.provide(Layer.merge(ConfigLive, LoggerLive.pipe(Layer.provide(ConfigLive))))
)
// Тип: Layer<Database, never, never>
// Программа может использовать ТОЛЬКО Database

// === С provideMerge: Config сохраняется ===

const AppConfigLayer = Layer.merge(ConfigLive, LoggerLive)
// Тип: Layer<Config | Logger, never, Config>

const AppWithProvideMerge = DatabaseLive.pipe(
  Layer.provide(AppConfigLayer),
  Layer.provideMerge(ConfigLive)
)
// Тип: Layer<Config | Database, never, never>
// Программа может использовать и Database, и Config!

// Теперь программа может обратиться к Config напрямую
const program = Effect.gen(function* () {
  const config = yield* Config
  const db = yield* Database
  const result = yield* db.query("SELECT 1")
  return { logLevel: config.logLevel, result }
})

const runnable = Effect.provide(program, AppWithProvideMerge)

Пошаговая сборка MainLive

Рассмотрим типичный паттерн сборки главного Layer:


class Config extends Context.Tag("Config")<Config, { readonly env: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}

const ConfigLive = Layer.succeed(Config, { env: "production" })
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
  const { env } = yield* Config
  return { log: (msg) => Effect.log(`[${env}] ${msg}`) }
}))
const DatabaseLive = Layer.effect(Database, Effect.gen(function* () {
  yield* Config
  yield* Logger
  return { query: (sql) => Effect.succeed({ sql }) }
}))

// Шаг 1: Config + Logger
const BaseLive = LoggerLive.pipe(
  Layer.provideMerge(ConfigLive)
)
// Тип: Layer<Config | Logger, never, never>

// Шаг 2: Database + сохраняем Config и Logger
const MainLive = DatabaseLive.pipe(
  Layer.provide(BaseLive),       // Database получает Config и Logger
  Layer.provideMerge(BaseLive)   // Config и Logger тоже доступны
)
// Тип: Layer<Config | Logger | Database, never, never>

Effect.provide с Layer

На уровне программы Layer предоставляется через Effect.provide:


declare const program: Effect.Effect<string, never, Database | Config>
declare const MainLive: Layer.Layer<Database | Config>

// Предоставление Layer программе
const runnable = Effect.provide(program, MainLive)
// Тип: Effect<string, never, never>

Множественное предоставление

// Можно предоставлять Layer по частям
const step1 = Effect.provide(program, DatabaseLayer)
const step2 = Effect.provide(step1, ConfigLayer)
// Каждый provide разрешает часть зависимостей

// Или в pipe
const runnable = program.pipe(
  Effect.provide(DatabaseLayer),
  Effect.provide(ConfigLayer)
)

Предоставление Layer к Effect напрямую (shorthand)

Effect поддерживает предоставление Layer прямо через Tag:


class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}

const ConfigLive = Layer.succeed(Config, { port: 3000 })

const program = Effect.gen(function* () {
  const config = yield* Config
  return config.port
})

// Краткая форма: Effect.provide с Layer
const result = Effect.provide(program, ConfigLive)

Разрешение сложных графов

Diamond dependency

Одна из классических проблем DI — «алмазная зависимость»:

        Config
       /      \
    Logger   Cache
       \      /
      Database

Logger и Cache оба зависят от Config. Database зависит от Logger и Cache. Вопрос: будет ли Config создан один или два раза?


class Config extends Context.Tag("Config")<Config, { readonly id: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly configId: string }>() {}
class Cache extends Context.Tag("Cache")<Cache, { readonly configId: string }>() {}
class Database extends Context.Tag("Database")<Database, { readonly deps: string }>() {}

const ConfigLive = Layer.effect(
  Config,
  Effect.gen(function* () {
    const id = `config-${Date.now()}`
    yield* Effect.log(`Config created: ${id}`)
    return { id }
  })
)

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return { configId: config.id }
  })
)

const CacheLive = Layer.effect(
  Cache,
  Effect.gen(function* () {
    const config = yield* Config
    return { configId: config.id }
  })
)

const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const logger = yield* Logger
    const cache = yield* Cache
    return { deps: `logger(${logger.configId}), cache(${cache.configId})` }
  })
)

// Сборка с правильной мемоизацией
const MainLive = DatabaseLive.pipe(
  Layer.provide(
    Layer.merge(
      LoggerLive.pipe(Layer.provide(ConfigLive)),
      CacheLive.pipe(Layer.provide(ConfigLive))
    )
  )
)

const program = Effect.gen(function* () {
  const db = yield* Database
  yield* Effect.log(`Database deps: ${db.deps}`)
})

Effect.runPromise(Effect.provide(program, MainLive))
// Output:
// Config created: config-1234567890  ← ОДИН раз!
// Database deps: logger(config-1234567890), cache(config-1234567890)
// Logger и Cache получили ОДИН И ТОТ ЖЕ Config

Это работает благодаря автоматической мемоизации Layer при глобальном предоставлении (подробнее в статье 06).


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

Создание «слоёв» архитектуры

Production-приложение обычно организовано в слои:

┌────────────────────────────────────────────┐
│               Application                   │
│  ┌──────────────────────────────────────┐  │
│  │            Domain Layer               │  │
│  │  UserService, OrderService, ...       │  │
│  │  ┌──────────────────────────────┐    │  │
│  │  │      Infrastructure Layer     │    │  │
│  │  │  Database, Cache, MQ, ...     │    │  │
│  │  │  ┌──────────────────────┐    │    │  │
│  │  │  │    Foundation Layer    │    │    │  │
│  │  │  │  Config, Logger, ...   │    │    │  │
│  │  │  └──────────────────────┘    │    │  │
│  │  └──────────────────────────────┘    │  │
│  └──────────────────────────────────────┘  │
└────────────────────────────────────────────┘

// === Foundation Tags ===
class Config extends Context.Tag("Config")<Config, { readonly env: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}

// === Infrastructure Tags ===
class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
class Cache extends Context.Tag("Cache")<Cache, { readonly get: (key: string) => Effect.Effect<string | null> }>() {}

// === Domain Tags ===
class UserService extends Context.Tag("UserService")<UserService, { readonly find: (id: string) => Effect.Effect<unknown> }>() {}

// === Foundation Layer ===
const ConfigLive = Layer.succeed(Config, { env: "production" })
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
  const config = yield* Config
  return { log: (msg) => Effect.log(`[${config.env}] ${msg}`) }
}))

const FoundationLive = LoggerLive.pipe(
  Layer.provideMerge(ConfigLive)
)
// Тип: Layer<Config | Logger, never, never>

// === Infrastructure Layer ===
const DatabaseLive = Layer.effect(Database, Effect.gen(function* () {
  const logger = yield* Logger
  yield* logger.log("Database initialized")
  return { query: (sql) => Effect.succeed({ sql }) }
}))

const CacheLive = Layer.effect(Cache, Effect.gen(function* () {
  const logger = yield* Logger
  yield* logger.log("Cache initialized")
  return { get: (_key) => Effect.succeed(null) }
}))

const InfraLive = Layer.merge(DatabaseLive, CacheLive).pipe(
  Layer.provideMerge(FoundationLive)
)
// Тип: Layer<Config | Logger | Database | Cache, never, never>

// === Domain Layer ===
const UserServiceLive = Layer.effect(UserService, Effect.gen(function* () {
  const db = yield* Database
  const cache = yield* Cache
  const logger = yield* Logger
  return {
    find: (id) => Effect.gen(function* () {
      yield* logger.log(`Finding user ${id}`)
      const cached = yield* cache.get(`user:${id}`)
      if (cached !== null) return JSON.parse(cached)
      return yield* db.query(`SELECT * FROM users WHERE id = '${id}'`)
    })
  }
}))

const DomainLive = UserServiceLive.pipe(
  Layer.provideMerge(InfraLive)
)
// Тип: Layer<Config | Logger | Database | Cache | UserService, never, never>

// === Финальная сборка ===
const MainLive = DomainLive
// Все зависимости разрешены, все сервисы доступны

API Reference

Layer.provide [STABLE]

function provide<ROut, E, RIn, ROut2, E2, RIn2>(
  self: Layer<ROut, E, RIn>,
  that: Layer<ROut2, E2, RIn2>
): Layer<ROut, E | E2, RIn2 | Exclude<RIn, ROut2>>

Предоставляет зависимости self из that. Выход содержит только сервисы self.

Layer.provideMerge [STABLE]

function provideMerge<ROut, E, RIn, ROut2, E2, RIn2>(
  self: Layer<ROut, E, RIn>,
  that: Layer<ROut2, E2, RIn2>
): Layer<ROut | ROut2, E | E2, RIn2 | Exclude<RIn, ROut2>>

Предоставляет зависимости self из that. Выход содержит сервисы как self, так и that.

Effect.provide [STABLE]

function provide<A, E, R, ROut, E2, RIn>(
  self: Effect<A, E, R>,
  layer: Layer<ROut, E2, RIn>
): Effect<A, E | E2, RIn | Exclude<R, ROut>>

Предоставляет Layer программе, удаляя из зависимостей всё что Layer производит.


Примеры

Пример: Микросервис с многоуровневой архитектурой


// === Errors ===
class AppError extends Data.TaggedError("AppError")<{
  readonly code: string
  readonly message: string
}> {}

// === Services ===
class Config extends Context.Tag("Config")<
  Config,
  {
    readonly dbHost: string
    readonly cacheEnabled: boolean
    readonly logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"
  }
>() {}

class Logger extends Context.Tag("Logger")<
  Logger,
  {
    readonly debug: (msg: string) => Effect.Effect<void>
    readonly info: (msg: string) => Effect.Effect<void>
    readonly warn: (msg: string) => Effect.Effect<void>
    readonly error: (msg: string) => Effect.Effect<void>
  }
>() {}

class EventBus extends Context.Tag("EventBus")<
  EventBus,
  {
    readonly publish: (event: string, payload: unknown) => Effect.Effect<void>
  }
>() {}

class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findAll: () => Effect.Effect<ReadonlyArray<{ id: string; name: string }>>
    readonly findById: (id: string) => Effect.Effect<{ id: string; name: string } | null>
  }
>() {}

class UserController extends Context.Tag("UserController")<
  UserController,
  {
    readonly getUsers: () => Effect.Effect<ReadonlyArray<{ id: string; name: string }>, AppError>
    readonly getUser: (id: string) => Effect.Effect<{ id: string; name: string }, AppError>
  }
>() {}

// === Implementations ===

const ConfigLive = Layer.succeed(Config, {
  dbHost: "localhost",
  cacheEnabled: true,
  logLevel: "INFO" as const
})

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    const levels = ["DEBUG", "INFO", "WARN", "ERROR"] as const
    const minLevel = levels.indexOf(config.logLevel)

    const shouldLog = (level: typeof levels[number]) =>
      levels.indexOf(level) >= minLevel

    const write = (level: typeof levels[number], msg: string) =>
      shouldLog(level)
        ? Effect.log(`[${level}] ${msg}`)
        : Effect.void

    return {
      debug: (msg) => write("DEBUG", msg),
      info: (msg) => write("INFO", msg),
      warn: (msg) => write("WARN", msg),
      error: (msg) => write("ERROR", msg)
    }
  })
)

const EventBusLive = Layer.effect(
  EventBus,
  Effect.gen(function* () {
    const logger = yield* Logger
    return {
      publish: (event, payload) =>
        logger.info(`Event: ${event} | Payload: ${JSON.stringify(payload)}`)
    }
  })
)

const UserRepositoryLive = Layer.effect(
  UserRepository,
  Effect.gen(function* () {
    const logger = yield* Logger
    const users: ReadonlyArray<{ id: string; name: string }> = [
      { id: "1", name: "Alice" },
      { id: "2", name: "Bob" },
      { id: "3", name: "Carol" }
    ] as const

    return {
      findAll: () =>
        Effect.gen(function* () {
          yield* logger.debug("Fetching all users")
          return users
        }),
      findById: (id) =>
        Effect.gen(function* () {
          yield* logger.debug(`Fetching user: ${id}`)
          return users.find((u) => u.id === id) ?? null
        })
    }
  })
)

const UserControllerLive = Layer.effect(
  UserController,
  Effect.gen(function* () {
    const repo = yield* UserRepository
    const eventBus = yield* EventBus
    const logger = yield* Logger

    return {
      getUsers: () =>
        Effect.gen(function* () {
          yield* logger.info("GET /users")
          const users = yield* repo.findAll()
          yield* eventBus.publish("users.listed", { count: users.length })
          return users
        }),
      getUser: (id) =>
        Effect.gen(function* () {
          yield* logger.info(`GET /users/${id}`)
          const user = yield* repo.findById(id)
          if (user === null) {
            return yield* Effect.fail(new AppError({
              code: "USER_NOT_FOUND",
              message: `User ${id} not found`
            }))
          }
          yield* eventBus.publish("user.viewed", { id })
          return user
        })
    }
  })
)

// === Layer Composition ===

// Foundation: Config + Logger
const FoundationLive = LoggerLive.pipe(
  Layer.provideMerge(ConfigLive)
)

// Infrastructure: EventBus + UserRepository (+ Foundation)
const InfraLive = Layer.merge(EventBusLive, UserRepositoryLive).pipe(
  Layer.provideMerge(FoundationLive)
)

// Application: UserController (+ всё остальное)
const AppLive = UserControllerLive.pipe(
  Layer.provideMerge(InfraLive)
)

// === Program ===

const program = Effect.gen(function* () {
  const controller = yield* UserController
  const users = yield* controller.getUsers()
  yield* Effect.log(`Found ${users.length} users`)

  const alice = yield* controller.getUser("1")
  yield* Effect.log(`Found user: ${alice.name}`)

  // Попытка найти несуществующего пользователя
  const unknown = yield* controller.getUser("999").pipe(
    Effect.catchTag("AppError", (e) =>
      Effect.gen(function* () {
        yield* Effect.log(`Error: ${e.code} - ${e.message}`)
        return { id: "0", name: "Unknown" }
      })
    )
  )
  return { users, alice, unknown }
})

Effect.runPromise(Effect.provide(program, AppLive)).then(console.log)

Упражнения

Упражнение

Линейная цепочка provide

Легко

Создайте 3 сервиса в линейной зависимости:

  • A (без зависимостей) → B (зависит от A) → C (зависит от B)
  • Используйте Layer.provide для полного разрешения
  • Напишите программу, которая использует C
Упражнение

provideMerge для сохранения доступа

Средне

Создайте приложение, где программа напрямую использует Config, Logger И UserService одновременно. Используйте provideMerge, чтобы все три были доступны.

Упражнение

Многоуровневая архитектура с переключаемыми реализациями

Сложно

Создайте приложение с 3 уровнями:

  1. Foundation: Config, Logger
  2. Infrastructure: Database, MessageQueue
  3. Domain: OrderService (зависит от Database, MQ, Logger)

Требования:

  • Две реализации Database: PostgresLive и InMemoryLive
  • Два профиля: ProductionLive и TestLive
  • Используйте provide/provideMerge для сборки обоих профилей
  • Программа должна работать одинаково с обоими профилями

🔗 Далее: Горизонтальная композиция: merge, mergeAll — объединение независимых Layer