Композиция 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
class A extends Context.Tag("A")<A, { readonly value: number }>() {}
class B extends Context.Tag("B")<B, { readonly doubled: number }>() {}
class C extends Context.Tag("C")<C, { readonly message: string }>() {}
const ALive = Layer.succeed(A, { value: 21 })
const BLive = Layer.effect(B, Effect.gen(function* () {
const a = yield* A
return { doubled: a.value * 2 }
}))
const CLive = Layer.effect(C, Effect.gen(function* () {
const b = yield* B
return { message: `The answer is ${b.doubled}` }
}))
const MainLive = CLive.pipe(
Layer.provide(BLive),
Layer.provide(ALive)
)
// Тип: Layer<C, never, never>
const program = Effect.gen(function* () {
const c = yield* C
return c.message
})
Effect.runPromise(Effect.provide(program, MainLive)).then(console.log)
// Output: The answer is 42provideMerge для сохранения доступа
Создайте приложение, где программа напрямую использует Config, Logger И UserService одновременно. Используйте provideMerge, чтобы все три были доступны.
class Config extends Context.Tag("Config")<Config, { readonly appName: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly info: (msg: string) => Effect.Effect<void> }>() {}
class UserService extends Context.Tag("UserService")<UserService, { readonly count: () => Effect.Effect<number> }>() {}
const ConfigLive = Layer.succeed(Config, { appName: "MyApp" })
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return { info: (msg) => Effect.log(`[${config.appName}] ${msg}`) }
}))
const UserServiceLive = Layer.effect(UserService, Effect.gen(function* () {
const logger = yield* Logger
yield* logger.info("UserService initialized")
return { count: () => Effect.succeed(42) }
}))
// Собираем с provideMerge — все сервисы доступны
const MainLive = UserServiceLive.pipe(
Layer.provide(LoggerLive.pipe(Layer.provide(ConfigLive))),
Layer.provideMerge(LoggerLive.pipe(Layer.provideMerge(ConfigLive)))
)
const program = Effect.gen(function* () {
const config = yield* Config // Доступен!
const logger = yield* Logger // Доступен!
const users = yield* UserService // Доступен!
yield* logger.info(`App: ${config.appName}`)
const count = yield* users.count()
yield* logger.info(`Users: ${count}`)
return count
})
Effect.runPromise(Effect.provide(program, MainLive)).then(console.log)Многоуровневая архитектура с переключаемыми реализациями
Создайте приложение с 3 уровнями:
- Foundation: Config, Logger
- Infrastructure: Database, MessageQueue
- Domain: OrderService (зависит от Database, MQ, Logger)
Требования:
- Две реализации Database: PostgresLive и InMemoryLive
- Два профиля: ProductionLive и TestLive
- Используйте provide/provideMerge для сборки обоих профилей
- Программа должна работать одинаково с обоими профилями
// === Tags ===
class Config extends Context.Tag("Config")<Config, { readonly env: string; readonly dbUrl: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly execute: (sql: string) => Effect.Effect<string> }>() {}
class MessageQueue extends Context.Tag("MessageQueue")<MessageQueue, { readonly send: (topic: string, msg: string) => Effect.Effect<void> }>() {}
class OrderService extends Context.Tag("OrderService")<OrderService, { readonly create: (item: string) => Effect.Effect<string> }>() {}
// === Foundation ===
const ConfigProd = Layer.succeed(Config, { env: "production", dbUrl: "postgres://prod-db:5432/orders" })
const ConfigTest = Layer.succeed(Config, { env: "test", dbUrl: "memory://" })
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return { log: (msg) => Effect.log(`[${config.env}] ${msg}`) }
}))
const foundationFrom = (config: Layer.Layer<Config>) =>
LoggerLive.pipe(Layer.provideMerge(config))
// === Infrastructure ===
const PostgresLive = Layer.effect(Database, Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
yield* logger.log(`Connecting to PostgreSQL: ${config.dbUrl}`)
return { execute: (sql) => Effect.succeed(`[PG] ${sql} → rows`) }
}))
const InMemoryDbLive = Layer.effect(Database, Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Using in-memory database")
const store = new Map<string, string>()
return {
execute: (sql) => Effect.sync(() => {
store.set(sql, "result")
return `[MEM] ${sql} → cached`
})
}
}))
const MessageQueueLive = Layer.effect(MessageQueue, Effect.gen(function* () {
const logger = yield* Logger
return { send: (topic, msg) => logger.log(`[MQ] ${topic}: ${msg}`) }
}))
const infraFrom = (db: Layer.Layer<Database, never, Config | Logger>, foundation: Layer.Layer<Config | Logger>) =>
Layer.merge(db, MessageQueueLive).pipe(
Layer.provideMerge(foundation)
)
// === Domain ===
const OrderServiceLive = Layer.effect(OrderService, Effect.gen(function* () {
const db = yield* Database
const mq = yield* MessageQueue
const logger = yield* Logger
return {
create: (item) => Effect.gen(function* () {
const result = yield* db.execute(`INSERT INTO orders (item) VALUES ('${item}')`)
yield* mq.send("orders.created", item)
yield* logger.log(`Order created: ${result}`)
return result
})
}
}))
// === Profiles ===
const FoundationProd = foundationFrom(ConfigProd)
const FoundationTest = foundationFrom(ConfigTest)
const InfraProd = infraFrom(PostgresLive, FoundationProd)
const InfraTest = infraFrom(InMemoryDbLive, FoundationTest)
const ProductionLive = OrderServiceLive.pipe(Layer.provideMerge(InfraProd))
const TestLive = OrderServiceLive.pipe(Layer.provideMerge(InfraTest))
// === Program (работает с обоими профилями) ===
const program = Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
const orders = yield* OrderService
yield* logger.log(`Starting in ${config.env} mode`)
const result = yield* orders.create("Widget")
return { env: config.env, result }
})
// Production
Effect.runPromise(Effect.provide(program, ProductionLive)).then(console.log)
// Test
Effect.runPromise(Effect.provide(program, TestLive)).then(console.log)🔗 Далее: Горизонтальная композиция: merge, mergeAll — объединение независимых Layer