Анатомия Effect
Центральный тип библиотеки Effect-ts — Effect<A, E, R> — представляет собой ленивое описание вычисления.
Теория
Что такое Effect?
Effect — это описание вычисления, а не само вычисление. Это фундаментальное отличие от императивного программирования, где код выполняется немедленно при вызове функции.
┌─── Тип успешного результата (Success)
│ ┌─── Тип ожидаемой ошибки (Error)
│ │ ┌─── Требуемые зависимости (Requirements)
▼ ▼ ▼
Effect<Success, Error, Requirements>
Ключевые характеристики Effect
Ленивость (Laziness)
Effect не выполняется при создании. Вычисление происходит только при явном запуске через Runtime:
// Это НЕ выполняет вычисление — только создаёт описание
const description = Effect.sync(() => {
console.log("Выполняюсь!")
return 42
})
// Никакого вывода в консоль!
// Вычисление произойдёт только при явном запуске
Иммутабельность (Immutability)
Каждая операция над Effect возвращает новый Effect, не изменяя исходный:
const original = Effect.succeed(10)
const doubled = Effect.map(original, (x) => x * 2)
const tripled = Effect.map(original, (x) => x * 3)
// original остаётся неизменным
// doubled и tripled — независимые вычисления
Композиционность (Composability)
Effect-ы можно комбинировать, создавая сложные workflow из простых блоков:
const workflow = pipe(
Effect.succeed(5),
Effect.map((x) => x * 2),
Effect.flatMap((x) => Effect.succeed(x + 1)),
Effect.map((x) => `Result: ${x}`)
)
Концепция ФП
Effect как функциональный IO
В функциональном программировании существует проблема: чистые функции не могут выполнять побочные эффекты (I/O, мутации, исключения). Решение — отложенные вычисления.
Концептуально Effect<A, E, R> можно представить как функцию:
type Effect<A, E, R> = (context: Context<R>) => E | A
Однако реальная реализация значительно сложнее и поддерживает:
- Синхронные и асинхронные вычисления
- Конкурентность и параллелизм
- Управление ресурсами
- Структурированную конкурентность через Fiber
Referential Transparency
Effect сохраняет референциальную прозрачность — описание вычисления всегда можно заменить на само описание без изменения семантики программы:
// Эти два варианта семантически эквивалентны
const program1 = Effect.flatMap(
Effect.succeed(5),
(x) => Effect.succeed(x * 2)
)
const five = Effect.succeed(5)
const program2 = Effect.flatMap(five, (x) => Effect.succeed(x * 2))
Связь с Monad
Effect реализует паттерн Monad из теории категорий:
| Операция | Effect API | Монадическая операция |
|---|---|---|
return / pure | Effect.succeed | Оборачивание значения |
bind / >>= | Effect.flatMap | Цепочка вычислений |
map | Effect.map | Трансформация результата |
// Монадические законы выполняются:
// 1. Left Identity: return a >>= f ≡ f a
const leftIdentity = Effect.flatMap(Effect.succeed(5), (x) => Effect.succeed(x * 2))
// эквивалентно Effect.succeed(10)
// 2. Right Identity: m >>= return ≡ m
const rightIdentity = Effect.flatMap(Effect.succeed(5), Effect.succeed)
// эквивалентно Effect.succeed(5)
// 3. Associativity: (m >>= f) >>= g ≡ m >>= (x => f x >>= g)
Анатомия типа Effect
Визуализация типа
┌────────────────────────────────────────────────────────────────┐
│ Effect<A, E, R> │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Success │ │ Error │ │ Requirements │ │
│ │ (A) │ │ (E) │ │ (R) │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────────────┤ │
│ │ Тип значения │ │ Ожидаемые │ │ Зависимости для │ │
│ │ при успехе │ │ ошибки │ │ выполнения │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ │
│ Порядок параметров: A, E, R (Success, Error, Requirements) │
│ │
└────────────────────────────────────────────────────────────────┘
Почему такой порядок параметров?
Effect использует порядок <A, E, R> (а не <R, E, A> как в некоторых других библиотеках) по нескольким причинам:
- Частичное применение типов — Success (
A) меняется чаще всего при композиции - Удобство чтения — при
mapиflatMapфокус на результате - TypeScript inference — компилятор лучше выводит типы в таком порядке
Type Parameters в деталях
Success (A) — Тип успешного результата
// A = number
const numberEffect: Effect.Effect<number, never, never> =
Effect.succeed(42)
// A = string
const stringEffect: Effect.Effect<string, never, never> =
Effect.succeed("hello")
// A = void — эффект не возвращает полезного значения
const voidEffect: Effect.Effect<void, never, never> =
Effect.sync(() => console.log("side effect"))
// A = never — эффект никогда не завершается успешно (бесконечный цикл)
const foreverEffect: Effect.Effect<never, never, never> =
Effect.forever(Effect.sync(() => {}))
Error (E) — Тип ожидаемой ошибки
// Определяем типизированные ошибки через Data.TaggedError
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
readonly status: number
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
// E = NetworkError
const networkCall: Effect.Effect<string, NetworkError, never> =
Effect.fail(new NetworkError({ url: "/api", status: 500 }))
// E = ValidationError
const validation: Effect.Effect<number, ValidationError, never> =
Effect.fail(new ValidationError({ field: "email", message: "Invalid format" }))
// E = never — эффект не может завершиться ожидаемой ошибкой
const infallible: Effect.Effect<number, never, never> =
Effect.succeed(42)
// E = NetworkError | ValidationError — объединение ошибок
const combined: Effect.Effect<string, NetworkError | ValidationError, never> =
Effect.gen(function* () {
const data = yield* networkCall
const parsed = yield* validation
return `${data}: ${parsed}`
})
⚠️ Важно: E = never означает, что Effect не может завершиться ожидаемой ошибкой. Это не гарантирует отсутствие дефектов (unexpected errors).
Requirements (R) — Требуемые зависимости
// Определяем сервис через Context.Tag
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>() {}
// R = Database
const dbEffect: Effect.Effect<ReadonlyArray<unknown>, never, Database> =
Effect.gen(function* () {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
// R = Logger
const logEffect: Effect.Effect<void, never, Logger> =
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Application started")
})
// R = Database | Logger — требуются оба сервиса
const combined: Effect.Effect<void, never, Database | Logger> =
Effect.gen(function* () {
const db = yield* Database
const logger = yield* Logger
const users = yield* db.query("SELECT * FROM users")
yield* logger.log(`Found ${users.length} users`)
})
// R = never — эффект не требует зависимостей
const standalone: Effect.Effect<number, never, never> =
Effect.succeed(42)
Извлечение типов
Utility Types
Effect предоставляет утилитарные типы для извлечения компонентов из типа Effect:
class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
// Исходный эффект
declare const program: Effect.Effect<string, Error, Config>
// Извлечение типа успеха
type Success = Effect.Effect.Success<typeof program>
// ^? type Success = string
// Извлечение типа ошибки
type Err = Effect.Effect.Error<typeof program>
// ^? type Err = Error
// Извлечение типа требований
type Ctx = Effect.Effect.Context<typeof program>
// ^? type Ctx = Config
Практический пример извлечения типов
// Определяем доменные типы
interface User {
readonly id: string
readonly name: string
readonly email: string
}
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
readonly userId: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly cause: unknown
}> {}
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User, UserNotFoundError | DatabaseError>
}
>() {}
// Функция, возвращающая Effect
const getUser = (id: string): Effect.Effect<
User,
UserNotFoundError | DatabaseError,
UserRepository
> =>
Effect.gen(function* () {
const repo = yield* UserRepository
return yield* repo.findById(id)
})
// Извлекаем типы для переиспользования
type GetUserEffect = ReturnType<typeof getUser>
type GetUserSuccess = Effect.Effect.Success<GetUserEffect> // User
type GetUserError = Effect.Effect.Error<GetUserEffect> // UserNotFoundError | DatabaseError
type GetUserContext = Effect.Effect.Context<GetUserEffect> // UserRepository
// Используем извлечённые типы
const processUser = (user: GetUserSuccess): string =>
`User: ${user.name} <${user.email}>`
const handleError = (error: GetUserError): string => {
switch (error._tag) {
case "UserNotFoundError":
return `User ${error.userId} not found`
case "DatabaseError":
return `Database error: ${error.cause}`
}
}
Сравнение с Promise
Effect vs Promise
| Характеристика | Promise | Effect |
|---|---|---|
| Выполнение | Eager (немедленное) | Lazy (отложенное) |
| Типизация ошибок | Нет (unknown) | Да (E) |
| Зависимости | Нет | Да (R) |
| Отмена | AbortController | Встроенная (Interruption) |
| Retry | Ручная реализация | Встроенный Schedule |
| Конкурентность | Promise.all/race | Fiber, structured concurrency |
| Композиция | then/catch | map/flatMap/catchAll |
| Тестирование | Mock модулей | Подмена через Context |
Пример: одна и та же логика
// ═══════════════════════════════════════════════════════════════
// Promise-based код
// ═══════════════════════════════════════════════════════════════
async function fetchUserPromise(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`) // Тип ошибки теряется
}
return response.json()
}
async function mainPromise(): Promise<void> {
try {
const user = await fetchUserPromise("123")
console.log(user)
} catch (error) {
// error: unknown — компилятор не знает тип
console.error("Error:", error)
}
}
// ═══════════════════════════════════════════════════════════════
// Effect-based код
// ═══════════════════════════════════════════════════════════════
interface User {
readonly id: string
readonly name: string
}
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly url: string
}> {}
class ParseError extends Data.TaggedError("ParseError")<{
readonly cause: unknown
}> {}
const fetchUserEffect = (id: string): Effect.Effect<User, HttpError | ParseError> =>
Effect.gen(function* () {
const response = yield* Effect.tryPromise({
try: () => fetch(`/api/users/${id}`),
catch: (cause) => new ParseError({ cause })
})
if (!response.ok) {
return yield* Effect.fail(new HttpError({
status: response.status,
url: `/api/users/${id}`
}))
}
return yield* Effect.tryPromise({
try: () => response.json() as Promise<User>,
catch: (cause) => new ParseError({ cause })
})
})
const mainEffect: Effect.Effect<void, HttpError | ParseError> =
Effect.gen(function* () {
const user = yield* fetchUserEffect("123")
yield* Effect.log(`User: ${user.name}`)
})
// Типы ошибок известны на этапе компиляции!
// mainEffect: Effect.Effect<void, HttpError | ParseError, never>
Примеры
Пример 1: Базовые Effect с разными типами
// ═══════════════════════════════════════════════════════════════
// Effect<A, never, never> — успешный эффект без ошибок и зависимостей
// ═══════════════════════════════════════════════════════════════
const pureValue: Effect.Effect<number, never, never> =
Effect.succeed(42)
const computation: Effect.Effect<number, never, never> =
Effect.sync(() => Math.random() * 100)
// ═══════════════════════════════════════════════════════════════
// Effect<A, E, never> — эффект с типизированной ошибкой
// ═══════════════════════════════════════════════════════════════
class DivisionByZero extends Data.TaggedError("DivisionByZero")<{
readonly dividend: number
}> {}
const safeDivide = (a: number, b: number): Effect.Effect<number, DivisionByZero> =>
b === 0
? Effect.fail(new DivisionByZero({ dividend: a }))
: Effect.succeed(a / b)
// ═══════════════════════════════════════════════════════════════
// Effect<A, never, R> — эффект с зависимостью
// ═══════════════════════════════════════════════════════════════
class RandomService extends Context.Tag("RandomService")<
RandomService,
{ readonly nextInt: (max: number) => Effect.Effect<number> }
>() {}
const rollDice: Effect.Effect<number, never, RandomService> =
Effect.gen(function* () {
const random = yield* RandomService
return yield* random.nextInt(6)
})
// ═══════════════════════════════════════════════════════════════
// Effect<A, E, R> — полный тип со всеми параметрами
// ═══════════════════════════════════════════════════════════════
class Cache extends Context.Tag("Cache")<
Cache,
{
readonly get: (key: string) => Effect.Effect<string | null>
readonly set: (key: string, value: string) => Effect.Effect<void>
}
>() {}
class CacheError extends Data.TaggedError("CacheError")<{
readonly operation: "get" | "set"
readonly key: string
}> {}
const cachedComputation = (key: string): Effect.Effect<string, CacheError, Cache> =>
Effect.gen(function* () {
const cache = yield* Cache
const cached = yield* cache.get(key)
if (cached !== null) {
return cached
}
const computed = `computed-${Date.now()}`
yield* cache.set(key, computed)
return computed
})
Пример 2: Трансформация типов через композицию
// Исходный эффект
const base: Effect.Effect<number, never, never> = Effect.succeed(10)
// map изменяет только A
const mapped: Effect.Effect<string, never, never> = pipe(
base,
Effect.map((n) => `Value: ${n}`)
)
// mapError изменяет только E
const original: Effect.Effect<number, Error, never> =
Effect.fail(new Error("oops"))
const remapped: Effect.Effect<number, string, never> = pipe(
original,
Effect.mapError((e) => e.message)
)
// flatMap может изменить A и добавить E/R
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly info: (msg: string) => Effect.Effect<void> }
>() {}
class LogError extends Data.TaggedError("LogError")<{}> {}
const withLogging: Effect.Effect<string, LogError, Logger> = pipe(
Effect.succeed(42),
Effect.flatMap((n) =>
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.info(`Processing: ${n}`)
return `Result: ${n}`
})
)
)
// Результат: Effect<string, LogError, Logger>
Пример 3: Реальный сценарий — User Service
// ═══════════════════════════════════════════════════════════════
// Доменные типы
// ═══════════════════════════════════════════════════════════════
interface User {
readonly id: string
readonly email: string
readonly name: string
}
interface CreateUserInput {
readonly email: string
readonly name: string
}
// ═══════════════════════════════════════════════════════════════
// Типизированные ошибки
// ═══════════════════════════════════════════════════════════════
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
readonly userId: string
}> {}
class DuplicateEmailError extends Data.TaggedError("DuplicateEmailError")<{
readonly email: string
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
type UserServiceError = UserNotFoundError | DuplicateEmailError | ValidationError
// ═══════════════════════════════════════════════════════════════
// Сервис зависимости
// ═══════════════════════════════════════════════════════════════
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User | null>
readonly findByEmail: (email: string) => Effect.Effect<User | null>
readonly create: (input: CreateUserInput) => Effect.Effect<User>
}
>() {}
// ═══════════════════════════════════════════════════════════════
// Бизнес-логика с полной типизацией
// ═══════════════════════════════════════════════════════════════
const validateEmail = (email: string): Effect.Effect<string, ValidationError> =>
email.includes("@")
? Effect.succeed(email)
: Effect.fail(new ValidationError({
field: "email",
message: "Invalid email format"
}))
const getUser = (id: string): Effect.Effect<User, UserNotFoundError, UserRepository> =>
Effect.gen(function* () {
const repo = yield* UserRepository
const user = yield* repo.findById(id)
if (user === null) {
return yield* Effect.fail(new UserNotFoundError({ userId: id }))
}
return user
})
const createUser = (input: CreateUserInput): Effect.Effect<
User,
DuplicateEmailError | ValidationError,
UserRepository
> =>
Effect.gen(function* () {
// Валидация
yield* validateEmail(input.email)
// Проверка уникальности
const repo = yield* UserRepository
const existing = yield* repo.findByEmail(input.email)
if (existing !== null) {
return yield* Effect.fail(new DuplicateEmailError({ email: input.email }))
}
// Создание
return yield* repo.create(input)
})
// Тип полностью выведен компилятором:
// createUser: (input: CreateUserInput) => Effect<User, DuplicateEmailError | ValidationError, UserRepository>
// ═══════════════════════════════════════════════════════════════
// Реализация репозитория для тестов
// ═══════════════════════════════════════════════════════════════
const TestUserRepository = Layer.succeed(
UserRepository,
{
findById: (id) => Effect.succeed(
id === "1" ? { id: "1", email: "test@example.com", name: "Test" } : null
),
findByEmail: (email) => Effect.succeed(
email === "test@example.com" ? { id: "1", email, name: "Test" } : null
),
create: (input) => Effect.succeed({
id: crypto.randomUUID(),
...input
})
}
)
// ═══════════════════════════════════════════════════════════════
// Запуск программы
// ═══════════════════════════════════════════════════════════════
const program = Effect.gen(function* () {
// Попытка создать пользователя с существующим email
const result = yield* pipe(
createUser({ email: "new@example.com", name: "New User" }),
Effect.catchTag("DuplicateEmailError", (e) =>
Effect.succeed({ id: "fallback", email: e.email, name: "Fallback" })
),
Effect.catchTag("ValidationError", (e) =>
Effect.fail(new Error(`Validation failed: ${e.message}`))
)
)
return result
})
// Запуск с предоставлением зависимостей
const runnable = pipe(
program,
Effect.provide(TestUserRepository)
)
Effect.runPromise(runnable).then(console.log)
Упражнения
Упражнение 1.1: Определение типов
Определите типы для следующих сценариев:
import { Effect, Context, Data } from "effect"
// 1. Эффект, который возвращает число и никогда не падает
type Ex1 = Effect.Effect</* ??? */>
// 2. Эффект, который может вернуть строку или упасть с Error
type Ex2 = Effect.Effect</* ??? */>
// 3. Эффект без полезного результата (только побочные эффекты)
type Ex3 = Effect.Effect</* ??? */>
// 4. Эффект, требующий Logger и возвращающий boolean
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
type Ex4 = Effect.Effect</* ??? */>import { Effect, Context } from "effect"
// 1. Effect<number, never, never>
type Ex1 = Effect.Effect<number, never, never>
// 2. Effect<string, Error, never>
type Ex2 = Effect.Effect<string, Error, never>
// 3. Effect<void, never, never>
type Ex3 = Effect.Effect<void, never, never>
// 4. Effect<boolean, never, Logger>
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
type Ex4 = Effect.Effect<boolean, never, Logger>Упражнение 1.2: Извлечение типов
Используя utility types, извлеките Success, Error и Context из данного эффекта:
import { Effect, Context, Data } from "effect"
class DbError extends Data.TaggedError("DbError")<{ cause: unknown }> {}
class AuthService extends Context.Tag("AuthService")<AuthService, { verify: () => Effect.Effect<boolean> }>() {}
declare const authenticate: Effect.Effect<string, DbError, AuthService>
// Извлеките типы:
type SuccessType = /* ??? */
type ErrorType = /* ??? */
type ContextType = /* ??? */type SuccessType = Effect.Effect.Success<typeof authenticate> // string
type ErrorType = Effect.Effect.Error<typeof authenticate> // DbError
type ContextType = Effect.Effect.Context<typeof authenticate> // AuthServiceУпражнение 1.3: Моделирование доменных ошибок
Создайте модель для сервиса обработки платежей с типизированными ошибками:
import { Effect, Context, Data } from "effect"
// Требуется реализовать:
// 1. Интерфейс Payment
// 2. Три типа ошибок: InsufficientFunds, InvalidCard, PaymentGatewayError
// 3. Сервис PaymentGateway с методом processPayment
// 4. Функцию processPayment, которая использует сервис
// interface Payment { ... }
// class InsufficientFunds extends Data.TaggedError(...) { ... }
// class InvalidCard extends Data.TaggedError(...) { ... }
// class PaymentGatewayError extends Data.TaggedError(...) { ... }
// class PaymentGateway extends Context.Tag(...) { ... }
// const processPayment = (payment: Payment): Effect.Effect<...> => ...import { Effect, Context, Data } from "effect"
interface Payment {
readonly id: string
readonly amount: number
readonly currency: string
readonly cardNumber: string
}
class InsufficientFunds extends Data.TaggedError("InsufficientFunds")<{
readonly required: number
readonly available: number
}> {}
class InvalidCard extends Data.TaggedError("InvalidCard")<{
readonly cardNumber: string
readonly reason: string
}> {}
class PaymentGatewayError extends Data.TaggedError("PaymentGatewayError")<{
readonly code: string
readonly message: string
}> {}
type PaymentError = InsufficientFunds | InvalidCard | PaymentGatewayError
class PaymentGateway extends Context.Tag("PaymentGateway")<
PaymentGateway,
{
readonly charge: (payment: Payment) => Effect.Effect<string, PaymentError>
readonly refund: (transactionId: string) => Effect.Effect<void, PaymentGatewayError>
}
>() {}
const processPayment = (payment: Payment): Effect.Effect<
string,
PaymentError,
PaymentGateway
> =>
Effect.gen(function* () {
const gateway = yield* PaymentGateway
// Валидация карты (локальная)
if (!payment.cardNumber.startsWith("4")) {
return yield* Effect.fail(new InvalidCard({
cardNumber: payment.cardNumber,
reason: "Only Visa cards accepted"
}))
}
// Обработка через gateway
const transactionId = yield* gateway.charge(payment)
return transactionId
})
// Тип: Effect<string, PaymentError, PaymentGateway>Упражнение 1.4: Композиция сервисов
Реализуйте сложный workflow с несколькими зависимостями:
import { Effect, Context, Data, Layer, pipe } from "effect"
// Задание:
// 1. Создайте три сервиса: UserService, EmailService, AuditService
// 2. Создайте функцию registerUser, которая:
// - Создаёт пользователя через UserService
// - Отправляет welcome email через EmailService
// - Логирует событие через AuditService
// 3. Определите все возможные ошибки
// 4. Создайте тестовые Layer-ы для всех сервисов
// 5. Запустите программу
// Подсказка: финальный тип должен быть примерно таким:
// Effect<User, UserError | EmailError | AuditError, UserService | EmailService | AuditService>import { Effect, Context, Data, Layer, pipe } from "effect"
// ═══════════════════════════════════════════════════════════════
// Доменные типы
// ═══════════════════════════════════════════════════════════════
interface User {
readonly id: string
readonly email: string
readonly name: string
}
interface RegisterInput {
readonly email: string
readonly name: string
}
// ═══════════════════════════════════════════════════════════════
// Ошибки
// ═══════════════════════════════════════════════════════════════
class UserCreationError extends Data.TaggedError("UserCreationError")<{
readonly reason: string
}> {}
class EmailDeliveryError extends Data.TaggedError("EmailDeliveryError")<{
readonly recipient: string
readonly cause: unknown
}> {}
class AuditLogError extends Data.TaggedError("AuditLogError")<{
readonly event: string
}> {}
type RegistrationError = UserCreationError | EmailDeliveryError | AuditLogError
// ═══════════════════════════════════════════════════════════════
// Сервисы
// ═══════════════════════════════════════════════════════════════
class UserService extends Context.Tag("UserService")<
UserService,
{
readonly create: (input: RegisterInput) => Effect.Effect<User, UserCreationError>
}
>() {}
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
readonly sendWelcome: (user: User) => Effect.Effect<void, EmailDeliveryError>
}
>() {}
class AuditService extends Context.Tag("AuditService")<
AuditService,
{
readonly log: (event: string, data: unknown) => Effect.Effect<void, AuditLogError>
}
>() {}
// ═══════════════════════════════════════════════════════════════
// Бизнес-логика
// ═══════════════════════════════════════════════════════════════
const registerUser = (input: RegisterInput): Effect.Effect<
User,
RegistrationError,
UserService | EmailService | AuditService
> =>
Effect.gen(function* () {
const userService = yield* UserService
const emailService = yield* EmailService
const auditService = yield* AuditService
// Создаём пользователя
const user = yield* userService.create(input)
// Отправляем welcome email
yield* emailService.sendWelcome(user)
// Логируем событие
yield* auditService.log("USER_REGISTERED", { userId: user.id, email: user.email })
return user
})
// ═══════════════════════════════════════════════════════════════
// Тестовые реализации
// ═══════════════════════════════════════════════════════════════
const TestUserService = Layer.succeed(UserService, {
create: (input) => Effect.succeed({
id: crypto.randomUUID(),
email: input.email,
name: input.name
})
})
const TestEmailService = Layer.succeed(EmailService, {
sendWelcome: (user) => Effect.sync(() => {
console.log(`[EMAIL] Welcome sent to ${user.email}`)
})
})
const TestAuditService = Layer.succeed(AuditService, {
log: (event, data) => Effect.sync(() => {
console.log(`[AUDIT] ${event}:`, data)
})
})
// Композиция всех слоёв
const TestLayer = Layer.mergeAll(
TestUserService,
TestEmailService,
TestAuditService
)
// ═══════════════════════════════════════════════════════════════
// Запуск
// ═══════════════════════════════════════════════════════════════
const program = pipe(
registerUser({ email: "user@example.com", name: "John Doe" }),
Effect.tap((user) => Effect.log(`Registered: ${user.name}`)),
Effect.provide(TestLayer)
)
Effect.runPromise(program).then(console.log)
/*
Output:
[EMAIL] Welcome sent to user@example.com
[AUDIT] USER_REGISTERED: { userId: '...', email: 'user@example.com' }
timestamp=... level=INFO fiber=#0 message="Registered: John Doe"
{ id: '...', email: 'user@example.com', name: 'John Doe' }
*/