Генераторы
Генераторы — это синтаксический сахар для написания последовательного асинхронного кода.
Теория
Проблема callback hell и цепочек
В функциональном программировании последовательные вычисления часто выражаются через flatMap:
// Проблема: глубокая вложенность
const program = pipe(
getUser(userId),
Effect.flatMap((user) =>
pipe(
getOrders(user.id),
Effect.flatMap((orders) =>
pipe(
calculateTotal(orders),
Effect.flatMap((total) =>
pipe(
applyDiscount(total, user.tier),
Effect.map((finalPrice) => ({
user,
orders,
total,
finalPrice
}))
)
)
)
)
)
)
)
Решение: генераторы
Генераторы позволяют “вытащить” значение из Effect и работать с ним как с обычной переменной:
// Чистый, читаемый код
const program = Effect.gen(function* () {
const user = yield* getUser(userId)
const orders = yield* getOrders(user.id)
const total = yield* calculateTotal(orders)
const finalPrice = yield* applyDiscount(total, user.tier)
return { user, orders, total, finalPrice }
})
Как это работает
┌─────────────────────────────────────────────────────────────────┐
│ Effect.gen(function* () { │
│ │
│ yield* effect1 ──► Приостановка, выполнение effect1 │
│ │ │
│ ▼ │
│ value1 = A ◄── Возобновление с результатом │
│ │ │
│ yield* effect2 ──► Приостановка, выполнение effect2 │
│ │ │
│ ▼ │
│ value2 = B ◄── Возобновление с результатом │
│ │ │
│ return C ──► Финальный результат │
│ │
│ }) │
│ │
│ Результат: Effect<C, E1 | E2, R1 | R2> │
│ (объединение всех ошибок и зависимостей) │
└─────────────────────────────────────────────────────────────────┘
Концепция ФП
Do-notation в Haskell
Effect.gen — это реализация do-notation из Haskell. В Haskell:
-- Haskell do-notation
program :: IO (User, [Order], Price)
program = do
user <- getUser userId
orders <- getOrders (userId user)
total <- calculateTotal orders
price <- applyDiscount total (tier user)
return (user, orders, price)
В Effect:
// TypeScript с Effect.gen
const program: Effect.Effect<
{ user: User; orders: Order[]; price: Price },
GetUserError | GetOrdersError | CalculateError | DiscountError,
UserService | OrderService
> = Effect.gen(function* () {
const user = yield* getUser(userId)
const orders = yield* getOrders(user.id)
const total = yield* calculateTotal(orders)
const price = yield* applyDiscount(total, user.tier)
return { user, orders, price }
})
Законы монады через генераторы
// Left Identity: return a >>= f ≡ f(a)
const leftIdentity = <A, B, E>(
a: A,
f: (a: A) => Effect.Effect<B, E>
) => {
// Эти два выражения эквивалентны:
const via_gen = Effect.gen(function* () {
const x = yield* Effect.succeed(a)
return yield* f(x)
})
const direct = f(a)
return true // via_gen ≡ direct
}
// Right Identity: m >>= return ≡ m
const rightIdentity = <A, E>(m: Effect.Effect<A, E>) => {
// Эти два выражения эквивалентны:
const via_gen = Effect.gen(function* () {
const x = yield* m
return x
})
const direct = m
return true // via_gen ≡ direct
}
// Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g)
const associativity = <A, B, C, E1, E2>(
m: Effect.Effect<A, E1>,
f: (a: A) => Effect.Effect<B, E1>,
g: (b: B) => Effect.Effect<C, E2>
) => {
// Эти два выражения эквивалентны:
const leftAssoc = Effect.gen(function* () {
const a = yield* m
const b = yield* f(a)
return yield* g(b)
})
const rightAssoc = Effect.gen(function* () {
const a = yield* m
return yield* Effect.gen(function* () {
const b = yield* f(a)
return yield* g(b)
})
})
return true // leftAssoc ≡ rightAssoc
}
Effect.gen — основы
Синтаксис
// ─────────────────────────────────────────────────────────────────
// Базовый синтаксис Effect.gen
// ─────────────────────────────────────────────────────────────────
// Вариант 1: function* () { ... }
const program1 = Effect.gen(function* () {
const a = yield* Effect.succeed(1)
const b = yield* Effect.succeed(2)
return a + b
})
// ^? Effect<number, never, never>
// Вариант 2: с this для доступа к контексту (редко используется)
const program2 = Effect.gen({ context: "myContext" }, function* () {
const a = yield* Effect.succeed(1)
return a
})
Типизация
// TypeScript автоматически выводит типы
const inferred = Effect.gen(function* () {
const n = yield* Effect.succeed(42)
// ^? number
const s = yield* Effect.succeed("hello")
// ^? string
return { n, s }
})
// ^? Effect<{ n: number; s: string }, never, never>
// Можно явно указать тип возвращаемого значения
const explicit = Effect.gen(function* (): Generator<
Effect.Effect<unknown, Error>,
{ result: number },
unknown
> {
const n = yield* Effect.succeed(42)
return { result: n }
})
// Или через as const / satisfies
const typed = Effect.gen(function* () {
const n = yield* Effect.succeed(42)
return { result: n } as const
}) satisfies Effect.Effect<{ readonly result: 42 }, never, never>
Важно: yield* vs yield
// ✅ ПРАВИЛЬНО: yield* (yield star)
const correct = Effect.gen(function* () {
const value = yield* Effect.succeed(42)
// ^^^^^^ yield* извлекает значение из Effect
return value
})
// ❌ НЕПРАВИЛЬНО: yield (без звёздочки)
const incorrect = Effect.gen(function* () {
// @ts-expect-error
const value = yield Effect.succeed(42)
// yield без * НЕ извлекает значение, а возвращает Effect
return value
})
// yield* работает ТОЛЬКО с Effect (или другими "yieldable" типами)
const mixedTypes = Effect.gen(function* () {
// ✅ Effect
const a = yield* Effect.succeed(1)
// ✅ Option (если есть адаптер)
// const b = yield* Option.some(2)
// ❌ Обычные значения нельзя yield*
// const c = yield* 3 // Ошибка!
return a
})
Effect.fn — создание функций с генераторами
Зачем нужен Effect.fn
Effect.fn — это специализированный конструктор для создания функций, использующих генераторный синтаксис. Он предоставляет два ключевых преимущества:
- Улучшенные stack traces — при ошибке вы видите точное место определения и вызова функции
- Автоматическое создание span’ов — для трассировки и observability
┌─────────────────────────────────────────────────────────────────┐
│ Сравнение подходов │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Effect.gen + обычная функция: │
│ ───────────────────────────── │
│ const myFunc = (n: number) => │
│ Effect.gen(function* () { │
│ yield* Effect.fail(new Error("Boom")) │
│ }) │
│ │
│ Stack trace: "Error: Boom" │
│ (без информации о месте вызова) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Effect.fn: │
│ ────────── │
│ const myFunc = Effect.fn("myFunc")(function* (n: number) { │
│ yield* Effect.fail(new Error("Boom")) │
│ }) │
│ │
│ Stack trace: "Error: Boom │
│ at <anonymous> (index.ts:3:22) ← Место ошибки │
│ at myFunc (index.ts:1:23) ← Определение функции │
│ at myFunc (index.ts:7:16)" ← Место вызова │
│ │
└─────────────────────────────────────────────────────────────────┘
Базовый синтаксис Effect.fn
// ─────────────────────────────────────────────────────────────────
// Вариант 1: Без имени span'а (только улучшенные stack traces)
// ─────────────────────────────────────────────────────────────────
const divide = Effect.fn(function* (a: number, b: number) {
if (b === 0) {
yield* Effect.fail(new Error("Division by zero"))
}
return a / b
})
// ^? (a: number, b: number) => Effect<number, Error, never>
// ─────────────────────────────────────────────────────────────────
// Вариант 2: С именем span'а (stack traces + tracing)
// ─────────────────────────────────────────────────────────────────
const divideTraced = Effect.fn("divide")(function* (a: number, b: number) {
yield* Effect.annotateCurrentSpan("a", a)
yield* Effect.annotateCurrentSpan("b", b)
if (b === 0) {
yield* Effect.fail(new Error("Division by zero"))
}
return a / b
})
// ─────────────────────────────────────────────────────────────────
// Использование
// ─────────────────────────────────────────────────────────────────
const program = Effect.gen(function* () {
const result = yield* divide(10, 2)
// ^? number
return result
})
Effect.runPromise(program).then(console.log)
// Output: 5
Дженерики в Effect.fn
// ─────────────────────────────────────────────────────────────────
// Effect.fn с generic параметрами
// ─────────────────────────────────────────────────────────────────
const parseJson = Effect.fn("parseJson")(function* <T>(
json: string,
schema: Schema.Schema<T>
) {
const parsed = yield* Schema.decode(Schema.parseJson(schema))(json)
return parsed
})
// ^? <T>(json: string, schema: Schema<T>) => Effect<T, ParseError, never>
// ─────────────────────────────────────────────────────────────────
// Использование с конкретными типами
// ─────────────────────────────────────────────────────────────────
const UserSchema = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String
})
type User = Schema.Schema.Type<typeof UserSchema>
const program = Effect.gen(function* () {
const user = yield* parseJson<User>(
'{"id": 1, "name": "Alice", "email": "alice@example.com"}',
UserSchema
)
// ^? User
return user
})
Effect.fn как pipe-функция
Effect.fn также может работать как pipe, позволяя добавить трансформации к результату:
// ─────────────────────────────────────────────────────────────────
// Effect.fn с pipeline — добавляем delay после выполнения
// ─────────────────────────────────────────────────────────────────
const slowFetch = Effect.fn(
// Первый аргумент — генератор-функция
function* (url: string) {
yield* Effect.log(`Fetching: ${url}`)
return { data: "response" }
},
// Второй аргумент — трансформация результата (effect, ...args)
(effect, url) => Effect.delay(effect, Duration.seconds(1))
)
// ─────────────────────────────────────────────────────────────────
// Более сложный пример с retry и timeout
// ─────────────────────────────────────────────────────────────────
const resilientFetch = Effect.fn(
"resilientFetch",
)(
function* (url: string, retries: number) {
yield* Effect.log(`Attempting fetch: ${url}`)
// Имитация возможной ошибки
const random = yield* Effect.sync(() => Math.random())
if (random < 0.5) {
yield* Effect.fail(new Error("Network error"))
}
return { url, data: "success" }
},
// Pipeline: добавляем retry и timeout
(effect, url, retries) =>
effect.pipe(
Effect.retry({ times: retries }),
Effect.timeout(Duration.seconds(5)),
Effect.tap(() => Effect.log(`Successfully fetched: ${url}`))
)
)
const program = Effect.gen(function* () {
const result = yield* resilientFetch("https://api.example.com", 3)
return result
})
Effect.fnUntraced — без трассировки
Effect.fnUntraced идентичен Effect.fn, но не добавляет трассировку. Используйте его когда:
- Функция вызывается очень часто (hot path)
- Трассировка не нужна (внутренние утилиты)
- Нужна максимальная производительность
// ─────────────────────────────────────────────────────────────────
// Effect.fnUntraced — без overhead на трассировку
// ─────────────────────────────────────────────────────────────────
const fastAdd = Effect.fnUntraced(function* (a: number, b: number) {
return a + b
})
// ─────────────────────────────────────────────────────────────────
// Сравнение: traced vs untraced
// ─────────────────────────────────────────────────────────────────
// С трассировкой — для бизнес-логики и внешних API
const processOrder = Effect.fn("processOrder")(function* (orderId: string) {
yield* Effect.annotateCurrentSpan("orderId", orderId)
yield* Effect.log(`Processing order: ${orderId}`)
// ... бизнес-логика
return { status: "processed" }
})
// Без трассировки — для внутренних hot-path утилит
const calculateHash = Effect.fnUntraced(function* (data: string) {
// Вызывается миллионы раз, трассировка не нужна
let hash = 0
for (let i = 0; i < data.length; i++) {
hash = ((hash << 5) - hash + data.charCodeAt(i)) | 0
}
return hash
})
// ─────────────────────────────────────────────────────────────────
// Когда что использовать
// ─────────────────────────────────────────────────────────────────
//
// Effect.fn("name") → Публичные API, бизнес-логика, handlers
// Effect.fn → Внутренние функции, нужны stack traces
// Effect.fnUntraced → Hot paths, утилиты, максимальная скорость
Effect.fn в сервисах
Effect.fn особенно полезен при создании сервисов:
// ─────────────────────────────────────────────────────────────────
// Определение сервиса с Effect.fn
// ─────────────────────────────────────────────────────────────────
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User | null, DatabaseError>
readonly save: (user: User) => Effect.Effect<User, DatabaseError>
readonly delete: (id: string) => Effect.Effect<void, DatabaseError>
}
>() {}
interface User {
readonly id: string
readonly name: string
readonly email: string
}
class DatabaseError extends Error {
readonly _tag = "DatabaseError"
}
// ─────────────────────────────────────────────────────────────────
// Реализация с Effect.fn для каждого метода
// ─────────────────────────────────────────────────────────────────
const UserRepositoryLive = Layer.succeed(
UserRepository,
{
findById: Effect.fn("UserRepository.findById")(function* (id: string) {
yield* Effect.annotateCurrentSpan("userId", id)
yield* Effect.log(`Finding user: ${id}`)
// Имитация запроса к БД
yield* Effect.sleep("100 millis")
return id === "1"
? { id: "1", name: "Alice", email: "alice@example.com" }
: null
}),
save: Effect.fn("UserRepository.save")(function* (user: User) {
yield* Effect.annotateCurrentSpan("userId", user.id)
yield* Effect.annotateCurrentSpan("userEmail", user.email)
yield* Effect.log(`Saving user: ${user.id}`)
yield* Effect.sleep("150 millis")
return user
}),
delete: Effect.fn("UserRepository.delete")(function* (id: string) {
yield* Effect.annotateCurrentSpan("userId", id)
yield* Effect.log(`Deleting user: ${id}`)
yield* Effect.sleep("100 millis")
})
}
)
// ─────────────────────────────────────────────────────────────────
// Использование сервиса
// ─────────────────────────────────────────────────────────────────
const program = Effect.gen(function* () {
const repo = yield* UserRepository
const user = yield* repo.findById("1")
if (user) {
yield* Effect.log(`Found: ${user.name}`)
}
const newUser = yield* repo.save({
id: "2",
name: "Bob",
email: "bob@example.com"
})
return newUser
})
const runnable = program.pipe(Effect.provide(UserRepositoryLive))
Effect.runPromise(runnable).then(console.log)
Экспорт span’ов для Observability
При использовании Effect.fn("name") автоматически создаются span’ы, которые можно экспортировать:
ConsoleSpanExporter,
BatchSpanProcessor
} from "@opentelemetry/sdk-trace-base"
// Функция с трассировкой
const processPayment = Effect.fn("processPayment")(
function* (amount: number, currency: string) {
yield* Effect.annotateCurrentSpan("amount", amount)
yield* Effect.annotateCurrentSpan("currency", currency)
yield* Effect.log(`Processing payment: ${amount} ${currency}`)
yield* Effect.sleep("200 millis")
// Имитация ошибки для демонстрации
if (amount > 10000) {
yield* Effect.fail(new Error("Amount exceeds limit"))
}
return { transactionId: `txn-${Date.now()}`, amount, currency }
}
)
// Настройка OpenTelemetry
const NodeSdkLive = NodeSdk.layer(() => ({
resource: { serviceName: "payment-service" },
spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter())
}))
const program = processPayment(100, "USD")
// Запуск с экспортом span'ов
Effect.runFork(
program.pipe(
Effect.provide(NodeSdkLive),
Effect.catchAllCause(Effect.logError)
)
)
/*
Output в консоли будет содержать span с:
- name: "processPayment"
- attributes: { amount: 100, currency: "USD" }
- status: success/error
- duration
- stack trace при ошибке
*/
Таблица сравнения
| Подход | Трассировка | Stack traces | Когда использовать |
|---|---|---|---|
Effect.gen | ❌ | Базовые | Inline логика, простые computations |
Effect.fn | ❌ | ✅ Улучшенные | Внутренние функции, нужны хорошие stack traces |
Effect.fn("name") | ✅ Span | ✅ Улучшенные | Публичные API, бизнес-логика, сервисы |
Effect.fnUntraced | ❌ | ❌ | Hot paths, утилиты, максимальная производительность |
yield* — извлечение значений
Извлечение успешного значения
const program = Effect.gen(function* () {
// yield* "разворачивает" Effect и даёт доступ к значению
const user = yield* Effect.succeed({ id: 1, name: "Alice" })
// ^? { id: number; name: string }
// Можно работать с user как с обычной переменной
const greeting = `Hello, ${user.name}!`
// Можно делать yield* несколько раз
const timestamp = yield* Effect.succeed(Date.now())
return { greeting, timestamp }
})
Обработка ошибок внутри генератора
class ValidationError {
readonly _tag = "ValidationError"
constructor(readonly field: string, readonly message: string) {}
}
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly code: number) {}
}
const validateInput = (input: string): Effect.Effect<string, ValidationError> =>
input.length > 0
? Effect.succeed(input.trim())
: Effect.fail(new ValidationError("input", "Cannot be empty"))
const fetchData = (query: string): Effect.Effect<string, NetworkError> =>
Effect.succeed(`Data for: ${query}`)
// Ошибки автоматически накапливаются в union
const program = Effect.gen(function* () {
const validated = yield* validateInput("test")
// ^? string
// Если validateInput вернёт fail, генератор прервётся здесь
const data = yield* fetchData(validated)
// ^? string
// Если fetchData вернёт fail, генератор прервётся здесь
return data
})
// ^? Effect<string, ValidationError | NetworkError, never>
// Обработка ошибок
const handled = pipe(
program,
Effect.catchTag("ValidationError", (e) =>
Effect.succeed(`Validation failed: ${e.message}`)
),
Effect.catchTag("NetworkError", (e) =>
Effect.succeed(`Network error: ${e.code}`)
)
)
Условное выполнение
interface User {
readonly id: string
readonly name: string
readonly email: string
readonly premiumUntil?: Date
}
const getUser = (id: string): Effect.Effect<User, Error> =>
Effect.succeed({ id, name: "Alice", email: "alice@example.com" })
const getPremiumFeatures = (user: User): Effect.Effect<string[]> =>
Effect.succeed(["feature1", "feature2"])
// Условная логика внутри генератора
const program = Effect.gen(function* () {
const user = yield* getUser("123")
// Обычный if/else
if (user.premiumUntil && user.premiumUntil > new Date()) {
const features = yield* getPremiumFeatures(user)
return { user, features, premium: true as const }
}
return { user, features: [] as string[], premium: false as const }
})
// Early return с yield*
const programWithEarlyReturn = Effect.gen(function* () {
const user = yield* getUser("123")
// Если не премиум, сразу возвращаем
if (!user.premiumUntil || user.premiumUntil <= new Date()) {
return { user, features: [], premium: false as const }
}
// Этот код выполнится только для премиум пользователей
const features = yield* getPremiumFeatures(user)
return { user, features, premium: true as const }
})
Циклы внутри генератора
interface Item {
readonly id: string
readonly name: string
}
const fetchItem = (id: string): Effect.Effect<Item, Error> =>
Effect.succeed({ id, name: `Item ${id}` })
// ❌ НЕ РАБОТАЕТ: yield* внутри обычного цикла
const badLoop = Effect.gen(function* () {
const ids = ["1", "2", "3"]
const items: Item[] = []
// Это НЕ будет работать как ожидается!
// for (const id of ids) {
// const item = yield* fetchItem(id) // Проблема!
// items.push(item)
// }
return items
})
// ✅ ПРАВИЛЬНО: используем Effect.forEach или Effect.all
const goodLoop = Effect.gen(function* () {
const ids = ["1", "2", "3"] as const
// Последовательное выполнение
const itemsSequential = yield* Effect.forEach(
ids,
(id) => fetchItem(id),
{ concurrency: 1 }
)
// Параллельное выполнение
const itemsParallel = yield* Effect.forEach(
ids,
(id) => fetchItem(id),
{ concurrency: "unbounded" }
)
return { sequential: itemsSequential, parallel: itemsParallel }
})
// ✅ ПРАВИЛЬНО: Effect.all для массива эффектов
const withAll = Effect.gen(function* () {
const effects = [
fetchItem("1"),
fetchItem("2"),
fetchItem("3")
] as const
const items = yield* Effect.all(effects)
return items
})
// ✅ ПРАВИЛЬНО: рекурсия для сложных случаев
const processWithRetry = (
ids: ReadonlyArray<string>,
maxRetries: number
): Effect.Effect<Item[], Error> =>
Effect.gen(function* () {
const results: Item[] = []
for (const id of ids) {
// Каждый элемент в отдельном генераторе
const item = yield* pipe(
fetchItem(id),
Effect.retry({ times: maxRetries })
)
results.push(item)
}
return results
})
Композиция типов
Автоматическое объединение ошибок
class DatabaseError {
readonly _tag = "DatabaseError"
constructor(readonly query: string) {}
}
class ValidationError {
readonly _tag = "ValidationError"
constructor(readonly field: string) {}
}
class AuthError {
readonly _tag = "AuthError"
constructor(readonly reason: string) {}
}
// Функции с разными типами ошибок
const validateInput = (x: string): Effect.Effect<string, ValidationError> =>
Effect.succeed(x)
const authenticate = (token: string): Effect.Effect<string, AuthError> =>
Effect.succeed("user-123")
const queryDatabase = (userId: string): Effect.Effect<object, DatabaseError> =>
Effect.succeed({ data: "result" })
// Типы ошибок автоматически объединяются
const program = Effect.gen(function* () {
const input = yield* validateInput("data")
// ^? Effect<string, ValidationError>
const userId = yield* authenticate("token")
// ^? Effect<string, AuthError>
const result = yield* queryDatabase(userId)
// ^? Effect<object, DatabaseError>
return { input, userId, result }
})
// ^? Effect<
// { input: string; userId: string; result: object },
// ValidationError | AuthError | DatabaseError, // ← Union!
// never
// >
Автоматическое объединение зависимостей
// Определяем сервисы
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> }
>() {}
class Cache extends Context.Tag("Cache")<
Cache,
{ readonly get: (key: string) => Effect.Effect<unknown> }
>() {}
// Функции с разными зависимостями
const logMessage = (msg: string): Effect.Effect<void, never, Logger> =>
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log(msg)
})
const fetchFromDb = (id: string): Effect.Effect<unknown, never, Database> =>
Effect.gen(function* () {
const db = yield* Database
return yield* db.query(`SELECT * FROM users WHERE id = '${id}'`)
})
const getFromCache = (key: string): Effect.Effect<unknown, never, Cache> =>
Effect.gen(function* () {
const cache = yield* Cache
return yield* cache.get(key)
})
// Зависимости автоматически объединяются
const program = Effect.gen(function* () {
yield* logMessage("Starting...")
// ^? Effect<void, never, Logger>
const cached = yield* getFromCache("user:123")
// ^? Effect<unknown, never, Cache>
const fresh = yield* fetchFromDb("123")
// ^? Effect<unknown, never, Database>
yield* logMessage("Done!")
return { cached, fresh }
})
// ^? Effect<
// { cached: unknown; fresh: unknown },
// never,
// Logger | Database | Cache // ← Union всех зависимостей!
// >
Сужение типов
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly code: number) {}
}
class ParseError {
readonly _tag = "ParseError"
constructor(readonly input: string) {}
}
const fetchData = (): Effect.Effect<string, NetworkError> =>
Effect.succeed('{"data": "value"}')
const parseJson = (s: string): Effect.Effect<object, ParseError> =>
Effect.try({
try: () => JSON.parse(s),
catch: () => new ParseError(s)
})
// Полный тип ошибки
const fullProgram = Effect.gen(function* () {
const raw = yield* fetchData()
const parsed = yield* parseJson(raw)
return parsed
})
// ^? Effect<object, NetworkError | ParseError, never>
// Обработка части ошибок сужает тип
const partiallyHandled = Effect.gen(function* () {
const raw = yield* pipe(
fetchData(),
Effect.catchTag("NetworkError", () => Effect.succeed("{}"))
)
// Теперь NetworkError обработан
const parsed = yield* parseJson(raw)
return parsed
})
// ^? Effect<object, ParseError, never> // ← Только ParseError!
// Полная обработка убирает ошибки
const fullyHandled = Effect.gen(function* () {
const raw = yield* pipe(
fetchData(),
Effect.catchTag("NetworkError", () => Effect.succeed("{}"))
)
const parsed = yield* pipe(
parseJson(raw),
Effect.catchTag("ParseError", () => Effect.succeed({}))
)
return parsed
})
// ^? Effect<object, never, never> // ← Ошибок нет!
Паттерны использования
Паттерн: Последовательные зависимые операции
interface User {
readonly id: string
readonly teamId: string
}
interface Team {
readonly id: string
readonly name: string
readonly ownerId: string
}
interface Permission {
readonly userId: string
readonly resource: string
readonly actions: ReadonlyArray<string>
}
const getUser = (id: string): Effect.Effect<User, Error> =>
Effect.succeed({ id, teamId: "team-1" })
const getTeam = (id: string): Effect.Effect<Team, Error> =>
Effect.succeed({ id, name: "Engineering", ownerId: "owner-1" })
const getPermissions = (userId: string, teamId: string): Effect.Effect<Permission[], Error> =>
Effect.succeed([{ userId, resource: "repo", actions: ["read", "write"] }])
// Зависимые операции — идеальный кейс для генераторов
const getUserContext = (userId: string) =>
Effect.gen(function* () {
// 1. Получаем пользователя
const user = yield* getUser(userId)
// 2. Используем user.teamId для получения команды
const team = yield* getTeam(user.teamId)
// 3. Используем оба значения для получения прав
const permissions = yield* getPermissions(user.id, team.id)
// 4. Возвращаем полный контекст
return {
user,
team,
permissions,
isTeamOwner: team.ownerId === user.id
} as const
})
Паттерн: Валидация с накоплением
interface FormData {
readonly name: string
readonly email: string
readonly age: string
}
interface ValidatedData {
readonly name: string
readonly email: string
readonly age: number
}
class FieldError {
constructor(
readonly field: string,
readonly message: string
) {}
}
const validateName = (name: string): Effect.Effect<string, FieldError> =>
name.trim().length >= 2
? Effect.succeed(name.trim())
: Effect.fail(new FieldError("name", "Name must be at least 2 characters"))
const validateEmail = (email: string): Effect.Effect<string, FieldError> =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? Effect.succeed(email.toLowerCase())
: Effect.fail(new FieldError("email", "Invalid email format"))
const validateAge = (age: string): Effect.Effect<number, FieldError> => {
const num = parseInt(age, 10)
return !isNaN(num) && num >= 0 && num <= 150
? Effect.succeed(num)
: Effect.fail(new FieldError("age", "Age must be a number between 0 and 150"))
}
// Валидация с накоплением всех ошибок
const validateForm = (data: FormData): Effect.Effect<ValidatedData, FieldError[]> =>
Effect.gen(function* () {
// Выполняем все валидации параллельно и собираем результаты
const results = yield* Effect.all(
[
pipe(validateName(data.name), Effect.either),
pipe(validateEmail(data.email), Effect.either),
pipe(validateAge(data.age), Effect.either)
],
{ concurrency: "unbounded" }
)
const [nameResult, emailResult, ageResult] = results
// Собираем ошибки
const errors: FieldError[] = []
if (Either.isLeft(nameResult)) errors.push(nameResult.left)
if (Either.isLeft(emailResult)) errors.push(emailResult.left)
if (Either.isLeft(ageResult)) errors.push(ageResult.left)
if (errors.length > 0) {
yield* Effect.fail(errors)
}
// Если ошибок нет, все результаты — Right
return {
name: (nameResult as Either.Right<string, FieldError>).right,
email: (emailResult as Either.Right<string, FieldError>).right,
age: (ageResult as Either.Right<number, FieldError>).right
}
})
Паттерн: Resource acquisition
interface Connection {
readonly id: string
readonly query: (sql: string) => Effect.Effect<unknown>
readonly close: () => Effect.Effect<void>
}
const acquireConnection = (): Effect.Effect<Connection> =>
Effect.gen(function* () {
yield* Effect.log("Acquiring connection...")
return {
id: `conn-${Date.now()}`,
query: (sql) => Effect.succeed({ rows: [] }),
close: () => Effect.log("Connection closed")
}
})
const releaseConnection = (conn: Connection): Effect.Effect<void> =>
conn.close()
// Используем Effect.acquireRelease для гарантированного освобождения
const withConnection = <A, E>(
use: (conn: Connection) => Effect.Effect<A, E>
): Effect.Effect<A, E, Scope.Scope> =>
Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(
acquireConnection(),
(conn) => releaseConnection(conn).pipe(Effect.orDie)
)
return yield* use(conn)
})
// Использование
const program = Effect.gen(function* () {
yield* Effect.log("Starting transaction...")
const result = yield* withConnection((conn) =>
Effect.gen(function* () {
yield* Effect.log(`Using connection ${conn.id}`)
const users = yield* conn.query("SELECT * FROM users")
const orders = yield* conn.query("SELECT * FROM orders")
return { users, orders }
})
)
yield* Effect.log("Transaction complete")
return result
})
// Запуск со Scope
const runnable = Effect.scoped(program)
Паттерн: Retry с логированием
class ApiError {
readonly _tag = "ApiError"
constructor(
readonly status: number,
readonly message: string
) {}
}
const callApi = (endpoint: string): Effect.Effect<object, ApiError> =>
Effect.gen(function* () {
yield* Effect.log(`Calling ${endpoint}...`)
// Симуляция случайного сбоя
if (Math.random() < 0.7) {
yield* Effect.fail(new ApiError(503, "Service unavailable"))
}
return { data: "success" }
})
// Retry с логированием каждой попытки
const callApiWithRetry = (endpoint: string) =>
Effect.gen(function* () {
let attempt = 0
const result = yield* pipe(
Effect.gen(function* () {
attempt++
yield* Effect.log(`Attempt ${attempt}`)
return yield* callApi(endpoint)
}),
Effect.retry(
Schedule.exponential(Duration.millis(100)).pipe(
Schedule.compose(Schedule.recurs(3)),
// Логируем между попытками
Schedule.tapOutput((duration) =>
Effect.log(`Retrying after ${Duration.toMillis(duration)}ms...`)
)
)
),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`All ${attempt} attempts failed: ${error.message}`)
return { data: "fallback", error: error.message }
})
)
)
yield* Effect.log(`Final result after ${attempt} attempt(s)`)
return result
})
Сравнение с pipe
Когда использовать pipe
// ✅ pipe хорош для простых линейных трансформаций
const withPipe = pipe(
Effect.succeed(10),
Effect.map((n) => n * 2),
Effect.map((n) => n.toString()),
Effect.map((s) => `Result: ${s}`)
)
// ✅ pipe хорош для одиночных операций
const singleOp = pipe(
someEffect,
Effect.timeout(Duration.seconds(5)),
Effect.retry(Schedule.recurs(3))
)
// ✅ pipe хорош когда не нужны промежуточные значения
const noIntermediates = pipe(
fetchUser(id),
Effect.flatMap(getOrders),
Effect.flatMap(calculateTotal)
)
Когда использовать Effect.gen
// ✅ gen хорош для зависимых операций с промежуточными значениями
const withGen = Effect.gen(function* () {
const user = yield* fetchUser(id)
const orders = yield* getOrders(user.id)
const total = yield* calculateTotal(orders)
// Нужны все три значения для результата
return {
user,
orderCount: orders.length,
total,
averageOrderValue: orders.length > 0 ? total / orders.length : 0
}
})
// ✅ gen хорош для условной логики
const conditional = Effect.gen(function* () {
const user = yield* getUser(id)
if (user.role === "admin") {
const adminData = yield* getAdminDashboard(user.id)
return { type: "admin" as const, data: adminData }
}
const userData = yield* getUserDashboard(user.id)
return { type: "user" as const, data: userData }
})
// ✅ gen хорош для сложной бизнес-логики
const businessLogic = Effect.gen(function* () {
const order = yield* getOrder(orderId)
// Проверяем статус
if (order.status !== "pending") {
yield* Effect.fail(new OrderNotPendingError(orderId))
}
// Получаем связанные данные
const customer = yield* getCustomer(order.customerId)
const inventory = yield* checkInventory(order.items)
// Бизнес-правила
if (inventory.hasOutOfStock) {
yield* notifyCustomer(customer.email, "outOfStock")
yield* Effect.fail(new OutOfStockError(inventory.outOfStockItems))
}
// Обработка
const payment = yield* processPayment(order, customer)
const shipment = yield* createShipment(order, customer)
return {
order,
payment,
shipment,
estimatedDelivery: shipment.estimatedDate
}
})
Комбинирование подходов
// Можно комбинировать gen и pipe
const combined = Effect.gen(function* () {
// Используем pipe для операторов
const user = yield* pipe(
fetchUser(id),
Effect.timeout(Duration.seconds(5)),
Effect.retry({ times: 3 }),
Effect.catchTag("TimeoutException", () =>
Effect.succeed({ id, name: "Unknown", cached: true })
)
)
// Обычная логика в генераторе
if (user.cached) {
yield* Effect.log("Using cached user data")
return { user, fresh: false }
}
// Ещё один pipe
const orders = yield* pipe(
getOrders(user.id),
Effect.map((orders) => orders.filter((o) => o.status === "active"))
)
return { user, orders, fresh: true }
})
// Или gen внутри pipe
const genInsidePipe = pipe(
getConfig(),
Effect.flatMap((config) =>
Effect.gen(function* () {
const db = yield* connectToDb(config.dbUrl)
const cache = yield* connectToCache(config.cacheUrl)
return { db, cache }
})
),
Effect.tap(({ db, cache }) =>
Effect.log(`Connected to ${db.name} and ${cache.name}`)
)
)
Do-notation
Effect.Do — альтернативный синтаксис
Effect.Do предоставляет способ накопления значений без генераторов:
// Do-notation style
const withDo = pipe(
Effect.Do,
Effect.bind("user", () => getUser(id)),
Effect.bind("orders", ({ user }) => getOrders(user.id)),
Effect.bind("total", ({ orders }) => calculateTotal(orders)),
Effect.let("count", ({ orders }) => orders.length),
Effect.map(({ user, orders, total, count }) => ({
user,
orders,
total,
averageValue: count > 0 ? total / count : 0
}))
)
// Эквивалент с генератором
const withGen = Effect.gen(function* () {
const user = yield* getUser(id)
const orders = yield* getOrders(user.id)
const total = yield* calculateTotal(orders)
const count = orders.length
return {
user,
orders,
total,
averageValue: count > 0 ? total / count : 0
}
})
API Do-notation
// ─────────────────────────────────────────────────────────────────
// Effect.Do — начало цепочки, возвращает Effect<{}>
// ─────────────────────────────────────────────────────────────────
const start = Effect.Do
// ^? Effect<{}, never, never>
// ─────────────────────────────────────────────────────────────────
// Effect.bind — добавить эффект, результат добавляется в объект
// ─────────────────────────────────────────────────────────────────
const withBind = pipe(
Effect.Do,
Effect.bind("a", () => Effect.succeed(1)),
Effect.bind("b", ({ a }) => Effect.succeed(a + 1)),
Effect.bind("c", ({ a, b }) => Effect.succeed(a + b))
)
// ^? Effect<{ a: number; b: number; c: number }, never, never>
// ─────────────────────────────────────────────────────────────────
// Effect.let — добавить чистое значение (не Effect)
// ─────────────────────────────────────────────────────────────────
const withLet = pipe(
Effect.Do,
Effect.bind("items", () => Effect.succeed([1, 2, 3])),
Effect.let("count", ({ items }) => items.length),
Effect.let("sum", ({ items }) => items.reduce((a, b) => a + b, 0))
)
// ^? Effect<{ items: number[]; count: number; sum: number }, never, never>
// ─────────────────────────────────────────────────────────────────
// Effect.bindTo — преобразовать Effect<A> в Effect<{ name: A }>
// ─────────────────────────────────────────────────────────────────
const withBindTo = pipe(
Effect.succeed(42),
Effect.bindTo("answer")
)
// ^? Effect<{ answer: number }, never, never>
// ─────────────────────────────────────────────────────────────────
// Комбинирование
// ─────────────────────────────────────────────────────────────────
const complex = pipe(
getUser(id),
Effect.bindTo("user"),
Effect.bind("orders", ({ user }) => getOrders(user.id)),
Effect.let("hasOrders", ({ orders }) => orders.length > 0),
Effect.bind("recommendations", ({ user, hasOrders }) =>
hasOrders
? getRecommendations(user.id)
: Effect.succeed([])
),
Effect.map(({ user, orders, recommendations }) => ({
user,
orderCount: orders.length,
recommendations
}))
)
Когда использовать Do-notation
// ✅ Do хорош когда нужно накопить несколько значений
// и каждое следующее зависит от предыдущих
// ❌ Избегайте Do для простых линейных цепочек
const tooSimple = pipe(
Effect.Do,
Effect.bind("x", () => Effect.succeed(1)),
Effect.map(({ x }) => x * 2)
) // Лучше просто pipe с map
// ✅ Do хорош для "accumulator" паттерна
const accumulator = pipe(
Effect.Do,
Effect.bind("config", () => loadConfig()),
Effect.bind("db", ({ config }) => connectDb(config.dbUrl)),
Effect.bind("cache", ({ config }) => connectCache(config.cacheUrl)),
Effect.bind("metrics", ({ config }) => setupMetrics(config.metricsUrl)),
Effect.map(({ db, cache, metrics }) => ({
db,
cache,
metrics,
// Все три нужны для создания сервиса
}))
)
Продвинутые техники
Вложенные генераторы
// Генераторы можно вкладывать друг в друга
const outer = Effect.gen(function* () {
yield* Effect.log("Outer start")
const innerResult = yield* Effect.gen(function* () {
yield* Effect.log("Inner start")
const a = yield* Effect.succeed(1)
const b = yield* Effect.succeed(2)
yield* Effect.log("Inner end")
return a + b
})
yield* Effect.log(`Inner result: ${innerResult}`)
yield* Effect.log("Outer end")
return innerResult * 2
})
Генераторы с сервисами
// Определение сервиса
class Logger extends Context.Tag("Logger")<
Logger,
{
readonly info: (msg: string) => Effect.Effect<void>
readonly error: (msg: string, err?: unknown) => Effect.Effect<void>
}
>() {}
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User | null>
readonly save: (user: User) => Effect.Effect<User>
}
>() {}
interface User {
readonly id: string
readonly name: string
readonly email: string
}
// Использование сервисов в генераторе
const updateUserEmail = (
userId: string,
newEmail: string
): Effect.Effect<User, Error, Logger | UserRepository> =>
Effect.gen(function* () {
// Получаем сервисы
const logger = yield* Logger
const repo = yield* UserRepository
yield* logger.info(`Updating email for user ${userId}`)
// Используем сервисы
const user = yield* repo.findById(userId)
if (user === null) {
yield* logger.error(`User ${userId} not found`)
yield* Effect.fail(new Error(`User ${userId} not found`))
}
const updatedUser = { ...user!, email: newEmail }
const saved = yield* repo.save(updatedUser)
yield* logger.info(`Email updated for user ${userId}`)
return saved
})
// Реализации сервисов
const LoggerLive = Layer.succeed(Logger, {
info: (msg) => Effect.sync(() => console.log(`[INFO] ${msg}`)),
error: (msg, err) => Effect.sync(() => console.error(`[ERROR] ${msg}`, err))
})
const UserRepositoryLive = Layer.succeed(UserRepository, {
findById: (id) => Effect.succeed({ id, name: "Alice", email: "old@example.com" }),
save: (user) => Effect.succeed(user)
})
// Запуск
const program = pipe(
updateUserEmail("user-123", "new@example.com"),
Effect.provide(Layer.merge(LoggerLive, UserRepositoryLive))
)
Параллельное выполнение внутри генератора
interface User { readonly id: string; readonly name: string }
interface Order { readonly id: string; readonly total: number }
interface Recommendation { readonly id: string; readonly title: string }
const fetchUser = (id: string): Effect.Effect<User, Error> =>
pipe(
Effect.succeed({ id, name: "Alice" }),
Effect.delay(Duration.millis(100))
)
const fetchOrders = (userId: string): Effect.Effect<Order[], Error> =>
pipe(
Effect.succeed([{ id: "o1", total: 100 }]),
Effect.delay(Duration.millis(150))
)
const fetchRecommendations = (userId: string): Effect.Effect<Recommendation[], Error> =>
pipe(
Effect.succeed([{ id: "r1", title: "Product A" }]),
Effect.delay(Duration.millis(200))
)
// Параллельное выполнение независимых операций
const getUserDashboard = (userId: string) =>
Effect.gen(function* () {
// Сначала получаем пользователя (нужен для дальнейших запросов)
const user = yield* fetchUser(userId)
// Затем параллельно загружаем orders и recommendations
const [orders, recommendations] = yield* Effect.all(
[
fetchOrders(user.id),
fetchRecommendations(user.id)
],
{ concurrency: "unbounded" }
)
return {
user,
orders,
recommendations,
totalSpent: orders.reduce((sum, o) => sum + o.total, 0)
}
})
// Ещё более параллельный вариант
const getUserDashboardParallel = (userId: string) =>
Effect.gen(function* () {
// Если user не нужен для других запросов, можно всё параллельно
const { user, orders, recommendations } = yield* Effect.all(
{
user: fetchUser(userId),
orders: fetchOrders(userId),
recommendations: fetchRecommendations(userId)
},
{ concurrency: "unbounded" }
)
return {
user,
orders,
recommendations,
totalSpent: orders.reduce((sum, o) => sum + o.total, 0)
}
})
Обработка Option и Either в генераторах
// Effect.gen работает с Option через специальные операторы
const withOption = Effect.gen(function* () {
const maybeValue: Option.Option<number> = Option.some(42)
// Вариант 1: Преобразовать Option в Effect
const value = yield* Effect.fromOption(maybeValue, () => "Value is None")
// Вариант 2: Использовать Option.getOrElse
const valueOrDefault = Option.getOrElse(maybeValue, () => 0)
return { value, valueOrDefault }
})
// Effect.gen работает с Either
const withEither = Effect.gen(function* () {
const result: Either.Either<string, number> = Either.right(42)
// Преобразовать Either в Effect
const value = yield* Effect.fromEither(result)
return value
})
// Практический пример: поиск с fallback
const findUserOrCreate = (
email: string
): Effect.Effect<User, Error> =>
Effect.gen(function* () {
// findByEmail возвращает Option
const maybeUser = yield* findUserByEmail(email)
// Если нашли — возвращаем
if (Option.isSome(maybeUser)) {
return maybeUser.value
}
// Иначе создаём нового
yield* Effect.log(`User with email ${email} not found, creating...`)
const newUser = yield* createUser({ email, name: email.split("@")[0] })
return newUser
})
interface User {
readonly id: string
readonly name: string
readonly email: string
}
const findUserByEmail = (email: string): Effect.Effect<Option.Option<User>> =>
Effect.succeed(Option.none())
const createUser = (data: { email: string; name: string }): Effect.Effect<User, Error> =>
Effect.succeed({ id: "new-user", ...data })
Примеры
Пример 1: HTTP запрос с полной обработкой
// ═══════════════════════════════════════════════════════════════════
// Типы ошибок
// ═══════════════════════════════════════════════════════════════════
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
readonly cause: unknown
}> {}
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly statusText: string
readonly url: string
}> {}
class ParseError extends Data.TaggedError("ParseError")<{
readonly body: string
readonly error: unknown
}> {}
class TimeoutError extends Data.TaggedError("TimeoutError")<{
readonly url: string
readonly timeout: Duration.Duration
}> {}
type FetchError = NetworkError | HttpError | ParseError | TimeoutError
// ═══════════════════════════════════════════════════════════════════
// HTTP клиент
// ═══════════════════════════════════════════════════════════════════
interface FetchOptions {
readonly method?: "GET" | "POST" | "PUT" | "DELETE"
readonly headers?: Record<string, string>
readonly body?: unknown
readonly timeout?: Duration.Duration
}
const fetchJson = <T>(
url: string,
options: FetchOptions = {}
): Effect.Effect<T, FetchError> =>
Effect.gen(function* () {
const timeout = options.timeout ?? Duration.seconds(30)
yield* Effect.log(`Fetching ${url}...`)
// 1. Выполняем запрос
const response = yield* pipe(
Effect.tryPromise({
try: () => fetch(url, {
method: options.method ?? "GET",
headers: {
"Content-Type": "application/json",
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
}),
catch: (cause) => new NetworkError({ url, cause })
}),
Effect.timeout(timeout),
Effect.catchTag("TimeoutException", () =>
Effect.fail(new TimeoutError({ url, timeout }))
)
)
// 2. Проверяем HTTP статус
if (!response.ok) {
yield* Effect.log(`HTTP error: ${response.status} ${response.statusText}`)
yield* Effect.fail(new HttpError({
status: response.status,
statusText: response.statusText,
url
}))
}
// 3. Читаем тело
const text = yield* Effect.tryPromise({
try: () => response.text(),
catch: (cause) => new NetworkError({ url, cause })
})
// 4. Парсим JSON
const data = yield* Effect.try({
try: () => JSON.parse(text) as T,
catch: (error) => new ParseError({ body: text.slice(0, 200), error })
})
yield* Effect.log(`Successfully fetched ${url}`)
return data
})
// ═══════════════════════════════════════════════════════════════════
// Использование
// ═══════════════════════════════════════════════════════════════════
interface Post {
readonly id: number
readonly title: string
readonly body: string
readonly userId: number
}
interface User {
readonly id: number
readonly name: string
readonly email: string
}
const getPostWithAuthor = (postId: number) =>
Effect.gen(function* () {
// Получаем пост
const post = yield* pipe(
fetchJson<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`),
Effect.retry(
Schedule.exponential(Duration.millis(100)).pipe(
Schedule.compose(Schedule.recurs(3))
)
)
)
yield* Effect.log(`Got post: ${post.title}`)
// Получаем автора
const author = yield* fetchJson<User>(
`https://jsonplaceholder.typicode.com/users/${post.userId}`
)
yield* Effect.log(`Got author: ${author.name}`)
return {
post,
author,
summary: `"${post.title}" by ${author.name}`
}
})
// С обработкой ошибок
const program = pipe(
getPostWithAuthor(1),
Effect.catchTags({
NetworkError: (e) =>
Effect.succeed({
post: null,
author: null,
summary: `Network error for ${e.url}`
}),
HttpError: (e) =>
Effect.succeed({
post: null,
author: null,
summary: `HTTP ${e.status} for ${e.url}`
}),
ParseError: (e) =>
Effect.succeed({
post: null,
author: null,
summary: `Parse error: ${e.body.slice(0, 50)}...`
}),
TimeoutError: (e) =>
Effect.succeed({
post: null,
author: null,
summary: `Timeout after ${Duration.toSeconds(e.timeout)}s for ${e.url}`
})
}),
Effect.tap((result) => Effect.log(`Result: ${result.summary}`))
)
Пример 2: Пакетная обработка с прогрессом
// ═══════════════════════════════════════════════════════════════════
// Типы
// ═══════════════════════════════════════════════════════════════════
interface ProcessingResult<T> {
readonly successful: ReadonlyArray<T>
readonly failed: ReadonlyArray<{ readonly item: T; readonly error: string }>
readonly totalProcessed: number
readonly totalFailed: number
readonly duration: Duration.Duration
}
interface ProgressInfo {
readonly current: number
readonly total: number
readonly percentage: number
readonly estimatedRemaining: Duration.Duration | null
}
// ═══════════════════════════════════════════════════════════════════
// Batch processor
// ═══════════════════════════════════════════════════════════════════
const processBatch = <T, R>(
items: ReadonlyArray<T>,
process: (item: T) => Effect.Effect<R, Error>,
options: {
readonly batchSize: number
readonly onProgress?: (progress: ProgressInfo) => Effect.Effect<void>
readonly concurrency?: number
}
): Effect.Effect<ProcessingResult<R>> =>
Effect.gen(function* () {
const startTime = yield* Effect.sync(() => Date.now())
const concurrency = options.concurrency ?? 5
// Состояние прогресса
const processedRef = yield* Ref.make(0)
const successfulRef = yield* Ref.make<R[]>([])
const failedRef = yield* Ref.make<Array<{ item: T; error: string }>>([])
// Разбиваем на батчи
const batches = Chunk.toReadonlyArray(
Chunk.chunksOf(Chunk.fromIterable(items), options.batchSize)
)
yield* Effect.log(`Processing ${items.length} items in ${batches.length} batches`)
// Обрабатываем батчи последовательно
for (const batch of batches) {
const batchItems = Chunk.toReadonlyArray(batch)
// Внутри батча — параллельно
yield* Effect.forEach(
batchItems,
(item) =>
Effect.gen(function* () {
const result = yield* pipe(
process(item),
Effect.either
)
// Обновляем состояние
yield* Ref.update(processedRef, (n) => n + 1)
if (result._tag === "Right") {
yield* Ref.update(successfulRef, (arr) => [...arr, result.right])
} else {
yield* Ref.update(failedRef, (arr) => [
...arr,
{ item, error: result.left.message }
])
}
// Отправляем прогресс
if (options.onProgress) {
const current = yield* Ref.get(processedRef)
const elapsed = Date.now() - startTime
const rate = current / elapsed
const remaining = items.length - current
yield* options.onProgress({
current,
total: items.length,
percentage: Math.round((current / items.length) * 100),
estimatedRemaining:
rate > 0
? Duration.millis(remaining / rate)
: null
})
}
}),
{ concurrency }
)
}
// Собираем результат
const endTime = yield* Effect.sync(() => Date.now())
const successful = yield* Ref.get(successfulRef)
const failed = yield* Ref.get(failedRef)
return {
successful,
failed,
totalProcessed: successful.length,
totalFailed: failed.length,
duration: Duration.millis(endTime - startTime)
}
})
// ═══════════════════════════════════════════════════════════════════
// Использование
// ═══════════════════════════════════════════════════════════════════
interface Order {
readonly id: string
readonly amount: number
}
interface ProcessedOrder {
readonly orderId: string
readonly status: "processed"
readonly processedAt: Date
}
const processOrder = (order: Order): Effect.Effect<ProcessedOrder, Error> =>
Effect.gen(function* () {
// Симуляция обработки
yield* Effect.sleep(Duration.millis(Math.random() * 100))
// Случайная ошибка для демонстрации
if (Math.random() < 0.1) {
yield* Effect.fail(new Error(`Failed to process order ${order.id}`))
}
return {
orderId: order.id,
status: "processed" as const,
processedAt: new Date()
}
})
const orders: ReadonlyArray<Order> = Array.from(
{ length: 100 },
(_, i) => ({ id: `order-${i + 1}`, amount: Math.random() * 1000 })
)
const program = Effect.gen(function* () {
yield* Effect.log("Starting batch processing...")
const result = yield* processBatch(orders, processOrder, {
batchSize: 10,
concurrency: 5,
onProgress: (progress) =>
Effect.log(
`Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`
)
})
yield* Effect.log(`
=== Processing Complete ===
Successful: ${result.totalProcessed}
Failed: ${result.totalFailed}
Duration: ${Duration.toSeconds(result.duration).toFixed(2)}s
`)
if (result.failed.length > 0) {
yield* Effect.log("Failed items:")
for (const failure of result.failed.slice(0, 5)) {
yield* Effect.log(` - ${failure.item.id}: ${failure.error}`)
}
if (result.failed.length > 5) {
yield* Effect.log(` ... and ${result.failed.length - 5} more`)
}
}
return result
})
Пример 3: Транзакционная бизнес-логика
// ═══════════════════════════════════════════════════════════════════
// Domain types
// ═══════════════════════════════════════════════════════════════════
interface Account {
readonly id: string
readonly balance: number
readonly currency: string
}
interface Transfer {
readonly id: string
readonly fromAccountId: string
readonly toAccountId: string
readonly amount: number
readonly currency: string
readonly status: "pending" | "completed" | "failed"
readonly timestamp: Date
}
// ═══════════════════════════════════════════════════════════════════
// Errors
// ═══════════════════════════════════════════════════════════════════
class InsufficientFundsError extends Data.TaggedError("InsufficientFundsError")<{
readonly accountId: string
readonly required: number
readonly available: number
}> {}
class AccountNotFoundError extends Data.TaggedError("AccountNotFoundError")<{
readonly accountId: string
}> {}
class CurrencyMismatchError extends Data.TaggedError("CurrencyMismatchError")<{
readonly expected: string
readonly actual: string
}> {}
type TransferError =
| InsufficientFundsError
| AccountNotFoundError
| CurrencyMismatchError
// ═══════════════════════════════════════════════════════════════════
// Services
// ═══════════════════════════════════════════════════════════════════
class AccountRepository extends Context.Tag("AccountRepository")<
AccountRepository,
{
readonly findById: (id: string) => Effect.Effect<Account | null>
readonly updateBalance: (id: string, newBalance: number) => Effect.Effect<Account>
}
>() {}
class TransferRepository extends Context.Tag("TransferRepository")<
TransferRepository,
{
readonly create: (transfer: Omit<Transfer, "id">) => Effect.Effect<Transfer>
readonly updateStatus: (
id: string,
status: Transfer["status"]
) => Effect.Effect<Transfer>
}
>() {}
class EventPublisher extends Context.Tag("EventPublisher")<
EventPublisher,
{
readonly publish: (event: { type: string; payload: unknown }) => Effect.Effect<void>
}
>() {}
// ═══════════════════════════════════════════════════════════════════
// Business logic
// ═══════════════════════════════════════════════════════════════════
const transferMoney = (
fromAccountId: string,
toAccountId: string,
amount: number,
currency: string
): Effect.Effect<
Transfer,
TransferError,
AccountRepository | TransferRepository | EventPublisher
> =>
Effect.gen(function* () {
const accountRepo = yield* AccountRepository
const transferRepo = yield* TransferRepository
const eventPublisher = yield* EventPublisher
yield* Effect.log(`Starting transfer: ${amount} ${currency} from ${fromAccountId} to ${toAccountId}`)
// 1. Валидация: получаем счета
const fromAccount = yield* Effect.gen(function* () {
const account = yield* accountRepo.findById(fromAccountId)
if (account === null) {
yield* Effect.fail(new AccountNotFoundError({ accountId: fromAccountId }))
}
return account!
})
const toAccount = yield* Effect.gen(function* () {
const account = yield* accountRepo.findById(toAccountId)
if (account === null) {
yield* Effect.fail(new AccountNotFoundError({ accountId: toAccountId }))
}
return account!
})
// 2. Проверка валюты
if (fromAccount.currency !== currency) {
yield* Effect.fail(new CurrencyMismatchError({
expected: currency,
actual: fromAccount.currency
}))
}
if (toAccount.currency !== currency) {
yield* Effect.fail(new CurrencyMismatchError({
expected: currency,
actual: toAccount.currency
}))
}
// 3. Проверка баланса
if (fromAccount.balance < amount) {
yield* Effect.fail(new InsufficientFundsError({
accountId: fromAccountId,
required: amount,
available: fromAccount.balance
}))
}
// 4. Создаём запись о трансфере
const transfer = yield* transferRepo.create({
fromAccountId,
toAccountId,
amount,
currency,
status: "pending",
timestamp: new Date()
})
yield* Effect.log(`Transfer ${transfer.id} created`)
// 5. Выполняем транзакцию
yield* pipe(
Effect.gen(function* () {
// Списываем со счёта отправителя
yield* accountRepo.updateBalance(
fromAccountId,
fromAccount.balance - amount
)
// Зачисляем на счёт получателя
yield* accountRepo.updateBalance(
toAccountId,
toAccount.balance + amount
)
// Обновляем статус трансфера
yield* transferRepo.updateStatus(transfer.id, "completed")
}),
// В случае ошибки — откатываем
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`Transfer ${transfer.id} failed, rolling back...`)
yield* transferRepo.updateStatus(transfer.id, "failed")
yield* Effect.fail(error)
})
)
)
// 6. Публикуем событие
yield* eventPublisher.publish({
type: "TransferCompleted",
payload: {
transferId: transfer.id,
fromAccountId,
toAccountId,
amount,
currency
}
})
yield* Effect.log(`Transfer ${transfer.id} completed successfully`)
return { ...transfer, status: "completed" as const }
})
// ═══════════════════════════════════════════════════════════════════
// In-memory implementations for testing
// ═══════════════════════════════════════════════════════════════════
const makeTestAccountRepository = Effect.gen(function* () {
const accounts = yield* Ref.make<Map<string, Account>>(
new Map([
["acc-1", { id: "acc-1", balance: 1000, currency: "USD" }],
["acc-2", { id: "acc-2", balance: 500, currency: "USD" }]
])
)
return AccountRepository.of({
findById: (id) =>
Effect.gen(function* () {
const map = yield* Ref.get(accounts)
return map.get(id) ?? null
}),
updateBalance: (id, newBalance) =>
Effect.gen(function* () {
const account = { id, balance: newBalance, currency: "USD" }
yield* Ref.update(accounts, (map) => new Map(map).set(id, account))
return account
})
})
})
const makeTestTransferRepository = Effect.gen(function* () {
const transfers = yield* Ref.make<Map<string, Transfer>>(new Map())
const idCounter = yield* Ref.make(0)
return TransferRepository.of({
create: (data) =>
Effect.gen(function* () {
const id = yield* Ref.updateAndGet(idCounter, (n) => n + 1)
const transfer: Transfer = { ...data, id: `transfer-${id}` }
yield* Ref.update(transfers, (map) => new Map(map).set(transfer.id, transfer))
return transfer
}),
updateStatus: (id, status) =>
Effect.gen(function* () {
const map = yield* Ref.get(transfers)
const transfer = map.get(id)!
const updated = { ...transfer, status }
yield* Ref.update(transfers, (m) => new Map(m).set(id, updated))
return updated
})
})
})
const TestEventPublisher = Layer.succeed(EventPublisher, {
publish: (event) => Effect.log(`Event: ${event.type}`)
})
// ═══════════════════════════════════════════════════════════════════
// Run
// ═══════════════════════════════════════════════════════════════════
const TestAccountRepositoryLayer = Layer.effect(
AccountRepository,
makeTestAccountRepository
)
const TestTransferRepositoryLayer = Layer.effect(
TransferRepository,
makeTestTransferRepository
)
const TestLayer = Layer.mergeAll(
TestAccountRepositoryLayer,
TestTransferRepositoryLayer,
TestEventPublisher
)
const program = pipe(
transferMoney("acc-1", "acc-2", 200, "USD"),
Effect.tap((transfer) =>
Effect.log(`Transfer result: ${JSON.stringify(transfer)}`)
),
Effect.catchTags({
AccountNotFoundError: (e) =>
Effect.log(`Account not found: ${e.accountId}`),
InsufficientFundsError: (e) =>
Effect.log(`Insufficient funds: need ${e.required}, have ${e.available}`),
CurrencyMismatchError: (e) =>
Effect.log(`Currency mismatch: expected ${e.expected}, got ${e.actual}`)
}),
Effect.provide(TestLayer)
)
Effect.runPromise(program)
Упражнения
Базовый: Конвертация pipe в gen
Перепишите следующий код с pipe на Effect.gen:
// Исходный код с pipe
const fetchUserData = (userId: string) =>
pipe(
getUser(userId),
Effect.flatMap((user) =>
pipe(
getOrders(user.id),
Effect.flatMap((orders) =>
pipe(
calculateTotal(orders),
Effect.map((total) => ({
user,
orders,
total,
averageOrderValue: orders.length > 0 ? total / orders.length : 0
}))
)
)
)
)
)
// TODO: Перепишите на Effect.gen
const fetchUserDataGen = (userId: string) =>
???
Решение
interface User {
readonly id: string
readonly name: string
}
interface Order {
readonly id: string
readonly amount: number
}
const getUser = (id: string): Effect.Effect<User, Error> =>
Effect.succeed({ id, name: "Alice" })
const getOrders = (userId: string): Effect.Effect<Order[], Error> =>
Effect.succeed([
{ id: "o1", amount: 100 },
{ id: "o2", amount: 200 }
])
const calculateTotal = (orders: Order[]): Effect.Effect<number, Error> =>
Effect.succeed(orders.reduce((sum, o) => sum + o.amount, 0))
// Решение с Effect.gen
const fetchUserDataGen = (userId: string) =>
Effect.gen(function* () {
const user = yield* getUser(userId)
const orders = yield* getOrders(user.id)
const total = yield* calculateTotal(orders)
return {
user,
orders,
total,
averageOrderValue: orders.length > 0 ? total / orders.length : 0
}
})
// Тест
Effect.runPromise(fetchUserDataGen("user-1")).then(console.log)
// { user: { id: 'user-1', name: 'Alice' }, orders: [...], total: 300, averageOrderValue: 150 }
Продвинутый: Поиск с фолбеками
Реализуйте функцию поиска данных, которая последовательно пробует разные источники:
interface Product {
readonly id: string
readonly name: string
readonly price: number
}
// Источники данных (от самого быстрого к самому медленному)
declare const getFromCache: (id: string) => Effect.Effect<Option.Option<Product>>
declare const getFromLocalDb: (id: string) => Effect.Effect<Option.Option<Product>, Error>
declare const getFromRemoteApi: (id: string) => Effect.Effect<Product, Error>
// TODO: Реализуйте функцию, которая:
// 1. Сначала проверяет кеш
// 2. Если в кеше нет — проверяет локальную БД
// 3. Если в локальной БД нет — запрашивает API
// 4. После успешного получения из API — сохраняет в кеш
// 5. Возвращает Product или ошибку NotFoundError
class NotFoundError {
readonly _tag = "NotFoundError"
constructor(readonly productId: string) {}
}
const findProduct = (
id: string
): Effect.Effect<Product, NotFoundError | Error> =>
???
Решение
interface Product {
readonly id: string
readonly name: string
readonly price: number
}
class NotFoundError {
readonly _tag = "NotFoundError"
constructor(readonly productId: string) {}
}
// Симуляция источников данных
const cache = new Map<string, Product>()
const getFromCache = (id: string): Effect.Effect<Option.Option<Product>> =>
Effect.gen(function* () {
yield* Effect.log(`[Cache] Looking for ${id}`)
yield* Effect.sleep(Duration.millis(10))
const product = cache.get(id)
if (product) {
yield* Effect.log(`[Cache] Found ${id}`)
return Option.some(product)
}
yield* Effect.log(`[Cache] Miss for ${id}`)
return Option.none()
})
const saveToCache = (product: Product): Effect.Effect<void> =>
Effect.gen(function* () {
yield* Effect.log(`[Cache] Saving ${product.id}`)
cache.set(product.id, product)
})
const getFromLocalDb = (id: string): Effect.Effect<Option.Option<Product>, Error> =>
Effect.gen(function* () {
yield* Effect.log(`[LocalDB] Looking for ${id}`)
yield* Effect.sleep(Duration.millis(50))
// Симуляция: только "local-1" есть в локальной БД
if (id === "local-1") {
yield* Effect.log(`[LocalDB] Found ${id}`)
return Option.some({ id, name: "Local Product", price: 99 })
}
yield* Effect.log(`[LocalDB] Not found ${id}`)
return Option.none()
})
const getFromRemoteApi = (id: string): Effect.Effect<Product, Error> =>
Effect.gen(function* () {
yield* Effect.log(`[API] Fetching ${id}`)
yield* Effect.sleep(Duration.millis(200))
// Симуляция: "api-1" есть в API, остальные — нет
if (id === "api-1") {
yield* Effect.log(`[API] Found ${id}`)
return { id, name: "Remote Product", price: 199 }
}
yield* Effect.log(`[API] Not found ${id}`)
yield* Effect.fail(new Error(`Product ${id} not found in API`))
})
// Решение
const findProduct = (
id: string
): Effect.Effect<Product, NotFoundError | Error> =>
Effect.gen(function* () {
yield* Effect.log(`=== Finding product ${id} ===`)
// 1. Проверяем кеш
const cached = yield* getFromCache(id)
if (Option.isSome(cached)) {
yield* Effect.log(`Found in cache!`)
return cached.value
}
// 2. Проверяем локальную БД
const local = yield* getFromLocalDb(id)
if (Option.isSome(local)) {
yield* Effect.log(`Found in local DB!`)
// Сохраняем в кеш для следующего раза
yield* saveToCache(local.value)
return local.value
}
// 3. Запрашиваем API
const remote = yield* pipe(
getFromRemoteApi(id),
Effect.catchAll((error) =>
Effect.fail(new NotFoundError(id))
)
)
yield* Effect.log(`Found in API!`)
// 4. Сохраняем в кеш
yield* saveToCache(remote)
return remote
})
// Тесты
const test = Effect.gen(function* () {
// Тест 1: Продукт есть в API
const result1 = yield* pipe(
findProduct("api-1"),
Effect.either
)
console.log("\nResult 1:", result1)
// Тест 2: Тот же продукт — теперь в кеше
const result2 = yield* pipe(
findProduct("api-1"),
Effect.either
)
console.log("\nResult 2:", result2)
// Тест 3: Продукт в локальной БД
const result3 = yield* pipe(
findProduct("local-1"),
Effect.either
)
console.log("\nResult 3:", result3)
// Тест 4: Продукта нет нигде
const result4 = yield* pipe(
findProduct("nonexistent"),
Effect.either
)
console.log("\nResult 4:", result4)
})
Effect.runPromise(test)
Экспертный: Saga Pattern с компенсациями
Реализуйте паттерн Saga для распределённой транзакции с автоматическими компенсациями:
// Saga step: action + compensation
interface SagaStep<A, E> {
readonly name: string
readonly action: Effect.Effect<A, E>
readonly compensation: (result: A) => Effect.Effect<void, never>
}
// TODO: Реализуйте Saga executor
// Требования:
// 1. Выполняет шаги последовательно
// 2. Если шаг падает — откатывает все предыдущие шаги в обратном порядке
// 3. Логирует каждый шаг и откат
// 4. Возвращает результаты всех шагов или ошибку с информацией о точке сбоя
class SagaError extends Data.TaggedError("SagaError")<{
readonly failedStep: string
readonly originalError: unknown
readonly compensatedSteps: ReadonlyArray<string>
}> {}
const executeSaga = <Steps extends readonly SagaStep<any, any>[]>(
name: string,
steps: Steps
): Effect.Effect<
{ [K in keyof Steps]: Steps[K] extends SagaStep<infer A, any> ? A : never },
SagaError
> =>
???
// Пример использования
const orderSaga = executeSaga("CreateOrder", [
{
name: "ReserveInventory",
action: reserveInventory(orderId, items),
compensation: (reservation) => releaseInventory(reservation.id)
},
{
name: "ChargePayment",
action: chargePayment(orderId, amount),
compensation: (payment) => refundPayment(payment.transactionId)
},
{
name: "CreateShipment",
action: createShipment(orderId, address),
compensation: (shipment) => cancelShipment(shipment.trackingId)
}
] as const)
Решение
// Saga step definition
interface SagaStep<A, E> {
readonly name: string
readonly action: Effect.Effect<A, E>
readonly compensation: (result: A) => Effect.Effect<void, never>
}
class SagaError extends Data.TaggedError("SagaError")<{
readonly failedStep: string
readonly originalError: unknown
readonly compensatedSteps: ReadonlyArray<string>
}> {}
// Saga executor
const executeSaga = <Steps extends readonly SagaStep<any, any>[]>(
name: string,
steps: Steps
): Effect.Effect<
{ [K in keyof Steps]: Steps[K] extends SagaStep<infer A, any> ? A : never },
SagaError
> =>
Effect.gen(function* () {
yield* Effect.log(`[Saga:${name}] Starting with ${steps.length} steps`)
// Храним результаты для возможной компенсации
const completedSteps = yield* Ref.make<Array<{
name: string
result: unknown
compensation: (result: unknown) => Effect.Effect<void, never>
}>>([])
const results: unknown[] = []
// Функция отката
const rollback = (failedStep: string, error: unknown) =>
Effect.gen(function* () {
const completed = yield* Ref.get(completedSteps)
const compensatedSteps: string[] = []
yield* Effect.log(`[Saga:${name}] Rolling back ${completed.length} steps...`)
// Откатываем в обратном порядке
for (const step of [...completed].reverse()) {
yield* Effect.log(`[Saga:${name}] Compensating: ${step.name}`)
yield* step.compensation(step.result)
compensatedSteps.push(step.name)
yield* Effect.log(`[Saga:${name}] Compensated: ${step.name}`)
}
return new SagaError({
failedStep,
originalError: error,
compensatedSteps
})
})
// Выполняем шаги последовательно
for (const step of steps) {
yield* Effect.log(`[Saga:${name}] Executing: ${step.name}`)
const result = yield* pipe(
step.action,
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[Saga:${name}] Step failed: ${step.name}`)
const sagaError = yield* rollback(step.name, error)
yield* Effect.fail(sagaError)
})
)
)
// Сохраняем для возможной компенсации
yield* Ref.update(completedSteps, (arr) => [
...arr,
{
name: step.name,
result,
compensation: step.compensation as (result: unknown) => Effect.Effect<void, never>
}
])
results.push(result)
yield* Effect.log(`[Saga:${name}] Completed: ${step.name}`)
}
yield* Effect.log(`[Saga:${name}] All steps completed successfully`)
return results as any
})
// ═══════════════════════════════════════════════════════════════════
// Пример: Order Saga
// ═══════════════════════════════════════════════════════════════════
interface Reservation {
readonly id: string
readonly items: ReadonlyArray<{ productId: string; quantity: number }>
}
interface Payment {
readonly transactionId: string
readonly amount: number
}
interface Shipment {
readonly trackingId: string
readonly address: string
}
const reserveInventory = (
orderId: string,
items: ReadonlyArray<{ productId: string; quantity: number }>
): Effect.Effect<Reservation, Error> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(100))
yield* Effect.log(`Reserving inventory for order ${orderId}`)
return {
id: `res-${orderId}`,
items
}
})
const releaseInventory = (reservationId: string): Effect.Effect<void, never> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(50))
yield* Effect.log(`Released inventory: ${reservationId}`)
})
const chargePayment = (
orderId: string,
amount: number,
shouldFail: boolean = false
): Effect.Effect<Payment, Error> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(150))
if (shouldFail) {
yield* Effect.fail(new Error("Payment declined"))
}
yield* Effect.log(`Charged payment for order ${orderId}`)
return {
transactionId: `pay-${orderId}`,
amount
}
})
const refundPayment = (transactionId: string): Effect.Effect<void, never> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(50))
yield* Effect.log(`Refunded payment: ${transactionId}`)
})
const createShipment = (
orderId: string,
address: string
): Effect.Effect<Shipment, Error> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(100))
yield* Effect.log(`Created shipment for order ${orderId}`)
return {
trackingId: `ship-${orderId}`,
address
}
})
const cancelShipment = (trackingId: string): Effect.Effect<void, never> =>
Effect.gen(function* () {
yield* Effect.sleep(Duration.millis(50))
yield* Effect.log(`Cancelled shipment: ${trackingId}`)
})
// Тест: успешная saga
const testSuccess = Effect.gen(function* () {
yield* Effect.log("\n=== TEST: Successful Saga ===\n")
const result = yield* executeSaga("CreateOrder", [
{
name: "ReserveInventory",
action: reserveInventory("order-1", [{ productId: "p1", quantity: 2 }]),
compensation: (res) => releaseInventory(res.id)
},
{
name: "ChargePayment",
action: chargePayment("order-1", 100),
compensation: (pay) => refundPayment(pay.transactionId)
},
{
name: "CreateShipment",
action: createShipment("order-1", "123 Main St"),
compensation: (ship) => cancelShipment(ship.trackingId)
}
] as const)
yield* Effect.log(`\nSaga result: ${JSON.stringify(result, null, 2)}`)
})
// Тест: saga с откатом
const testRollback = Effect.gen(function* () {
yield* Effect.log("\n=== TEST: Saga with Rollback ===\n")
const result = yield* pipe(
executeSaga("CreateOrder", [
{
name: "ReserveInventory",
action: reserveInventory("order-2", [{ productId: "p1", quantity: 2 }]),
compensation: (res) => releaseInventory(res.id)
},
{
name: "ChargePayment",
action: chargePayment("order-2", 100, true), // Эта операция упадёт
compensation: (pay) => refundPayment(pay.transactionId)
},
{
name: "CreateShipment",
action: createShipment("order-2", "123 Main St"),
compensation: (ship) => cancelShipment(ship.trackingId)
}
] as const),
Effect.either
)
if (result._tag === "Left") {
yield* Effect.log(`\nSaga failed:`)
yield* Effect.log(` Failed step: ${result.left.failedStep}`)
yield* Effect.log(` Original error: ${result.left.originalError}`)
yield* Effect.log(` Compensated: ${result.left.compensatedSteps.join(", ")}`)
}
})
// Запуск
const program = Effect.gen(function* () {
yield* testSuccess
yield* testRollback
})
Effect.runPromise(program)
Ключевые выводы
| Концепция | Описание | Когда использовать |
|---|---|---|
Effect.gen | Генератор для императивного стиля | Inline логика, простые computations |
Effect.fn | Создание функции-генератора | Внутренние функции, нужны stack traces |
Effect.fn("name") | Функция-генератор с трассировкой | Публичные API, бизнес-логика, сервисы |
Effect.fnUntraced | Функция-генератор без трассировки | Hot paths, утилиты, максимальная скорость |
yield* | Извлечение значения из Effect | Внутри Effect.gen и Effect.fn |
| Do-notation | Альтернатива с bind/let | Накопление значений без генератора |
pipe | Композиция функций | Линейные трансформации |
Правила использования генераторов
- yield только с Effect*: Нельзя использовать
yield*с обычными значениями - Типы объединяются: Ошибки и зависимости автоматически объединяются в union
- Циклы: Используйте
Effect.forEachилиEffect.allвместо циклов сyield* - Комбинируйте: Генераторы хорошо сочетаются с
pipeвнутри - Вложенность: Генераторы можно вкладывать друг в друга
- Трассировка: Используйте
Effect.fn("name")для сервисов и публичных API
Когда что использовать
| Ситуация | Подход |
|---|---|
| Простая линейная трансформация | pipe с map/flatMap |
| Inline логика с промежуточными значениями | Effect.gen |
| Условная логика | Effect.gen или Effect.fn |
| Сложная бизнес-логика | Effect.fn("name") |
| Методы сервисов с observability | Effect.fn("ServiceName.method") |
| Hot path, утилиты | Effect.fnUntraced |
| Накопление без генератора | Effect.Do с bind/let |