Exit: Результат выполнения Effect
Тип Exit<A, E> представляет конечный результат вычисления.
Теория
Зачем нужен Exit?
Когда Effect завершает выполнение, нам нужен способ представить результат. Exit<A, E> — это замороженный снимок результата вычисления:
- Success: вычисление успешно завершилось со значением типа
A - Failure: вычисление не удалось с причиной
Cause<E>
Структура Exit
Exit<A, E>
│
┌────────────┴────────────┐
│ │
Success<A> Failure<E>
│ │
value: A cause: Cause<E>
│
┌───────────┼───────────┐
│ │ │
Fail<E> Die Interrupt
│ │ │
(expected) (defect) (canceled)
Exit vs Either
На первый взгляд Exit похож на Either:
type Either<E, A> = Left<E> | Right<A>
type Exit<A, E> = Success<A> | Failure<E>
Ключевое отличие: Failure содержит не просто ошибку E, а полную Cause<E> — дерево всех причин сбоя.
| Аспект | Either<E, A> | Exit<A, E> |
|---|---|---|
| Ошибка | Одна ошибка E | Cause<E> — дерево причин |
| Дефекты | Не представлены | Включены через Cause.Die |
| Прерывания | Не представлены | Включены через Cause.Interrupt |
| Параллельные ошибки | Невозможно | Через Cause.Parallel |
Когда возникает Exit?
Exit является результатом запуска Effect:
// При синхронном запуске
Effect.runSyncExit(effect) // => Exit<A, E>
// При асинхронном запуске
Effect.runPromiseExit(effect) // => Promise<Exit<A, E>>
// При получении результата Fiber
fiber.await // => Effect<Exit<A, E>>
// При явном преобразовании
Effect.exit(effect) // => Effect<Exit<A, E>, never, R>
Концепция ФП
Exit как Sum Type
Exit<A, E> — это классический тип-сумма (discriminated union):
type Exit<A, E> = Success<A> | Failure<E>
interface Success<A> {
readonly _tag: "Success"
readonly value: A
}
interface Failure<E> {
readonly _tag: "Failure"
readonly cause: Cause<E>
}
Bifunctor для Exit
Exit является бифунктором — может трансформироваться по обоим параметрам:
// map: трансформация успешного значения
Exit.map: <A, B>(exit: Exit<A, E>, f: (a: A) => B) => Exit<B, E>
// mapError: трансформация ошибки
Exit.mapError: <E, E2>(exit: Exit<A, E>, f: (e: E) => E2) => Exit<A, E2>
// mapBoth: трансформация обоих
Exit.mapBoth: <A, B, E, E2>(
exit: Exit<A, E>,
options: { onSuccess: (a: A) => B; onFailure: (e: E) => E2 }
) => Exit<B, E2>
Monad для Exit
Exit также образует монаду:
// of / succeed: создание Success
Exit.succeed: <A>(value: A) => Exit<A, never>
// flatMap: цепочка вычислений
Exit.flatMap: <A, B, E>(exit: Exit<A, E>, f: (a: A) => Exit<B, E>) => Exit<B, E>
Это позволяет компоновать результаты:
const combined = pipe(
exit1,
Exit.flatMap((a) =>
pipe(
exit2,
Exit.map((b) => a + b)
)
)
)
Варианты Exit
Success<A>
Success<A> представляет успешное завершение с результатом типа A.
// Создание Success
const success = Exit.succeed(42)
// Проверка
console.log(Exit.isSuccess(success)) // true
// Извлечение значения
if (Exit.isSuccess(success)) {
console.log(success.value) // 42
}
📊 Структура Success:
Success<A>
│
└── value: A ← результат успешного вычисления
Failure<E>
Failure<E> представляет неуспешное завершение с причиной Cause<E>.
// Создание Failure с ожидаемой ошибкой
const failure = Exit.fail("Something went wrong")
// Создание Failure с дефектом
const defect = Exit.die(new Error("Unexpected!"))
// Создание Failure с произвольной Cause
const complex = Exit.failCause(
Cause.parallel(
Cause.fail("Error 1"),
Cause.fail("Error 2")
)
)
// Проверка
console.log(Exit.isFailure(failure)) // true
// Доступ к Cause
if (Exit.isFailure(failure)) {
console.log(failure.cause)
// { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' }
}
📊 Структура Failure:
Failure<E>
│
└── cause: Cause<E> ← полная информация о причинах сбоя
│
├── Fail<E> - ожидаемые ошибки
├── Die - неожиданные дефекты
├── Interrupt - прерывания
├── Sequential - последовательные ошибки
└── Parallel - параллельные ошибки
API Reference
Конструкторы
// Успешный результат
Exit.succeed<A>(value: A): Exit<A, never>
// Неуспешный результат с ожидаемой ошибкой
Exit.fail<E>(error: E): Exit<never, E>
// Неуспешный результат с дефектом
Exit.die(defect: unknown): Exit<never, never>
// Неуспешный результат с прерыванием
Exit.interrupt(fiberId: FiberId): Exit<never, never>
// Неуспешный результат с произвольной Cause
Exit.failCause<E>(cause: Cause<E>): Exit<never, E>
// Создание из Option
Exit.fromOption<A>(option: Option<A>): Exit<A, void>
// Создание из Either
Exit.fromEither<E, A>(either: Either<E, A>): Exit<A, E>
// Void успех
Exit.void: Exit<void, never>
// Unit успех
Exit.unit: Exit<void, never>
Type Guards
// Проверка на Success
Exit.isSuccess<A, E>(exit: Exit<A, E>): exit is Exit.Success<A, E>
// Проверка на Failure
Exit.isFailure<A, E>(exit: Exit<A, E>): exit is Exit.Failure<A, E>
// Проверка на прерывание
Exit.isInterrupted<A, E>(exit: Exit<A, E>): boolean
Деструкторы и извлечение
// Получение значения или undefined
Exit.getOrElse<A, E>(exit: Exit<A, E>, orElse: (cause: Cause<E>) => A): A
// Pattern matching
Exit.match<A, E, B, C>(exit: Exit<A, E>, options: {
readonly onSuccess: (a: A) => B
readonly onFailure: (cause: Cause<E>) => C
}): B | C
// Преобразование в Option (Success -> Some, Failure -> None)
Exit.toOption<A, E>(exit: Exit<A, E>): Option<A>
// Преобразование в Either
Exit.toEither<A, E>(exit: Exit<A, E>): Either<Cause<E>, A>
Трансформации
// map: преобразование успешного значения
Exit.map<A, B, E>(exit: Exit<A, E>, f: (a: A) => B): Exit<B, E>
// mapError: преобразование ошибок в Cause
Exit.mapError<A, E, E2>(exit: Exit<A, E>, f: (e: E) => E2): Exit<A, E2>
// mapBoth: преобразование обоих каналов
Exit.mapBoth<A, B, E, E2>(exit: Exit<A, E>, options: {
readonly onSuccess: (a: A) => B
readonly onFailure: (e: E) => E2
}): Exit<B, E2>
// flatMap: цепочка Exit
Exit.flatMap<A, B, E, E2>(
exit: Exit<A, E>,
f: (a: A) => Exit<B, E2>
): Exit<B, E | E2>
// flatten: развёртывание вложенного Exit
Exit.flatten<A, E, E2>(exit: Exit<Exit<A, E2>, E>): Exit<A, E | E2>
// as: замена успешного значения
Exit.as<A, B, E>(exit: Exit<A, E>, value: B): Exit<B, E>
// asVoid: приведение к void
Exit.asVoid<A, E>(exit: Exit<A, E>): Exit<void, E>
Композиция
// zip: комбинирование двух Exit (оба должны быть успешны)
Exit.zip<A, B, E, E2>(
self: Exit<A, E>,
that: Exit<B, E2>
): Exit<readonly [A, B], E | E2>
// zipWith: комбинирование с функцией
Exit.zipWith<A, B, C, E, E2>(
self: Exit<A, E>,
that: Exit<B, E2>,
f: (a: A, b: B) => C
): Exit<C, E | E2>
// zipLeft: возврат левого значения
Exit.zipLeft<A, B, E, E2>(
self: Exit<A, E>,
that: Exit<B, E2>
): Exit<A, E | E2>
// zipRight: возврат правого значения
Exit.zipRight<A, B, E, E2>(
self: Exit<A, E>,
that: Exit<B, E2>
): Exit<B, E | E2>
// all: комбинирование массива Exit
Exit.all<A, E>(exits: Iterable<Exit<A, E>>): Exit<ReadonlyArray<A>, E>
Утилиты
// Проверка на прерывание только
Exit.isInterrupted<A, E>(exit: Exit<A, E>): boolean
// Получение Cause (если Failure)
Exit.causeOption<A, E>(exit: Exit<A, E>): Option<Cause<E>>
// Применение функции при неудаче (для side-effects)
Exit.forEachEffect<A, E, B, R>(
exit: Exit<A, E>,
f: (a: A) => Effect<B, E, R>
): Effect<Option<B>, E, R>
Примеры
Пример 1: Базовое использование Exit
// Программа, которая может упасть
const program = Effect.gen(function* () {
const random = Math.random()
if (random < 0.5) {
return yield* Effect.fail("Bad luck!")
}
return "Success!"
})
// Получаем Exit вместо исключения
const main = async () => {
const exit = await Effect.runPromiseExit(program)
// Pattern matching по Exit
const result = Exit.match(exit, {
onSuccess: (value) => `✅ Got: ${value}`,
onFailure: (cause) => `❌ Failed: ${Cause.pretty(cause)}`
})
console.log(result)
}
main()
Пример 2: Работа с Exit в Effect.gen
// Конвертация Exit обратно в Effect
const exitToEffect = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<A, E> =>
Exit.isSuccess(exit)
? Effect.succeed(exit.value)
: Effect.failCause(exit.cause)
// Практическое использование
const program = Effect.gen(function* () {
// Выполняем эффект и получаем Exit
const exit = yield* Effect.exit(
Effect.fail("Something went wrong")
)
// Анализируем и трансформируем
if (Exit.isFailure(exit)) {
const failures = Cause.failures(exit.cause)
console.log("Errors:", [...failures])
// Можем вернуть fallback вместо ошибки
return "Fallback value"
}
return exit.value
})
Effect.runPromise(program).then(console.log)
// Errors: [ 'Something went wrong' ]
// Fallback value
Пример 3: Композиция Exit
// Два результата
const exit1: Exit.Exit<number, string> = Exit.succeed(10)
const exit2: Exit.Exit<number, string> = Exit.succeed(20)
const exit3: Exit.Exit<number, string> = Exit.fail("Error!")
// Комбинирование успешных
const combined = Exit.zipWith(
exit1,
exit2,
(a, b) => a + b
)
console.log(combined)
// { _id: 'Exit', _tag: 'Success', value: 30 }
// Комбинирование с ошибкой
const withError = Exit.zip(exit1, exit3)
console.log(withError)
// { _id: 'Exit', _tag: 'Failure', cause: { _tag: 'Fail', failure: 'Error!' } }
// Цепочка через flatMap
const chained = pipe(
exit1,
Exit.flatMap((a) => Exit.succeed(a * 2)),
Exit.flatMap((b) => Exit.succeed(b + 5))
)
console.log(chained)
// { _id: 'Exit', _tag: 'Success', value: 25 }
Пример 4: Трансформация ошибок в Exit
// Домен ошибок
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
class ApiError extends Data.TaggedError("ApiError")<{
readonly code: number
}> {}
type AppError = ValidationError | ApiError
// Exit с различными ошибками
const exit: Exit.Exit<string, AppError> = Exit.fail(
new ValidationError({ field: "email", message: "Invalid format" })
)
// Трансформация ошибки в строку для логирования
const mapped = Exit.mapError(exit, (error) => {
switch (error._tag) {
case "ValidationError":
return `Validation: ${error.field} - ${error.message}`
case "ApiError":
return `API Error: ${error.code}`
}
})
console.log(mapped)
// { _tag: 'Failure', cause: { _tag: 'Fail', failure: 'Validation: email - Invalid format' } }
// Трансформация обоих каналов
const transformed = Exit.mapBoth(exit, {
onSuccess: (s) => s.toUpperCase(),
onFailure: (error) => ({
errorCode: error._tag,
details: JSON.stringify(error)
})
})
console.log(transformed)
Пример 5: Exit в реальном сценарии — Retry с анализом
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly body: string
}> {}
// Симуляция HTTP запроса
const httpRequest = (url: string): Effect.Effect<string, HttpError> =>
Effect.gen(function* () {
const random = Math.random()
if (random < 0.7) {
return yield* Effect.fail(
new HttpError({
status: random < 0.3 ? 500 : 503,
body: "Server error"
})
)
}
return `Response from ${url}`
})
// Собираем все попытки и их результаты
interface Attempt {
readonly attemptNumber: number
readonly exit: Exit.Exit<string, HttpError>
readonly timestamp: number
}
const httpRequestWithRetry = (url: string): Effect.Effect<{
readonly result: string
readonly attempts: ReadonlyArray<Attempt>
}> =>
Effect.gen(function* () {
const attempts: Attempt[] = []
let attemptNumber = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attemptNumber++
const exit = yield* Effect.exit(httpRequest(url))
attempts.push({
attemptNumber,
exit,
timestamp: Date.now()
})
// Если успех — возвращаем значение
if (Exit.isSuccess(exit)) {
return exit.value
}
// Если ошибка — пробрасываем для retry
return yield* Effect.failCause(exit.cause)
}),
Schedule.recurs(3).pipe(
Schedule.intersect(Schedule.spaced("100 millis"))
)
).pipe(
Effect.catchAll((error) => Effect.succeed(`Failed after ${attemptNumber} attempts`))
)
return { result, attempts }
})
// Запуск
Effect.runPromise(httpRequestWithRetry("https://api.example.com"))
.then(({ result, attempts }) => {
console.log("Result:", result)
console.log("\nAttempts:")
for (const attempt of attempts) {
const status = Exit.isSuccess(attempt.exit) ? "✅" : "❌"
const details = Exit.match(attempt.exit, {
onSuccess: (v) => v,
onFailure: (cause) => {
const failure = Cause.failureOption(cause)
return failure._tag === "Some"
? `HTTP ${failure.value.status}`
: "Unknown error"
}
})
console.log(` ${status} Attempt ${attempt.attemptNumber}: ${details}`)
}
})
Пример 6: Exit.all для агрегации результатов
// Несколько Exit от разных операций
const exits: ReadonlyArray<Exit.Exit<number, string>> = [
Exit.succeed(1),
Exit.succeed(2),
Exit.succeed(3),
Exit.fail("Error in item 4"),
Exit.succeed(5)
]
// Попытка собрать все успешные
const combined = Exit.all(exits)
Exit.match(combined, {
onSuccess: (values) => {
console.log("All succeeded:", values)
},
onFailure: (cause) => {
console.log("Some failed:", Cause.pretty(cause))
}
})
// Some failed: Error: Error in item 4
// Если нужны все успешные, игнорируя ошибки
const successes = exits
.filter(Exit.isSuccess)
.map((exit) => exit.value)
console.log("Only successes:", successes)
// Only successes: [ 1, 2, 3, 5 ]
Упражнения
Создание и проверка Exit
// TODO: Реализуйте функции
// 1. Создать Success с числом 42
const createSuccess = (): Exit.Exit<number, never> => {
// Ваш код
}
// 2. Создать Failure с ошибкой "Not found"
const createFailure = (): Exit.Exit<never, string> => {
// Ваш код
}
// 3. Создать Failure с дефектом
const createDefect = (): Exit.Exit<never, never> => {
// Ваш код
}
// 4. Проверить, является ли Exit успехом и вернуть значение или default
const getValueOrDefault = <A, E>(
exit: Exit.Exit<A, E>,
defaultValue: A
): A => {
// Ваш код
}
const createSuccess = (): Exit.Exit<number, never> =>
Exit.succeed(42)
const createFailure = (): Exit.Exit<never, string> =>
Exit.fail("Not found")
const createDefect = (): Exit.Exit<never, never> =>
Exit.die(new Error("Unexpected null pointer"))
const getValueOrDefault = <A, E>(
exit: Exit.Exit<A, E>,
defaultValue: A
): A =>
Exit.getOrElse(exit, () => defaultValue)
// Альтернатива через pattern matching
const getValueOrDefaultAlt = <A, E>(
exit: Exit.Exit<A, E>,
defaultValue: A
): A =>
Exit.match(exit, {
onSuccess: (value) => value,
onFailure: () => defaultValue
})Pattern Matching
interface ProcessResult {
readonly status: "success" | "error" | "defect" | "interrupted"
readonly message: string
}
// TODO: Реализуйте функцию
// Должна возвращать ProcessResult на основе Exit
const analyzeExit = <A, E>(
exit: Exit.Exit<A, E>,
valueToString: (a: A) => string,
errorToString: (e: E) => string
): ProcessResult => {
// Ваш код
}
interface ProcessResult {
readonly status: "success" | "error" | "defect" | "interrupted"
readonly message: string
}
const analyzeExit = <A, E>(
exit: Exit.Exit<A, E>,
valueToString: (a: A) => string,
errorToString: (e: E) => string
): ProcessResult =>
Exit.match(exit, {
onSuccess: (value) => ({
status: "success" as const,
message: valueToString(value)
}),
onFailure: (cause) => {
// Проверяем на прерывание
if (Cause.isInterruptedOnly(cause)) {
return {
status: "interrupted" as const,
message: "Operation was interrupted"
}
}
// Проверяем на ожидаемую ошибку
const failureOpt = Cause.failureOption(cause)
if (Option.isSome(failureOpt)) {
return {
status: "error" as const,
message: errorToString(failureOpt.value)
}
}
// Проверяем на дефект
const defectOpt = Cause.dieOption(cause)
if (Option.isSome(defectOpt)) {
const defect = defectOpt.value
return {
status: "defect" as const,
message: defect instanceof Error ? defect.message : String(defect)
}
}
return {
status: "error" as const,
message: "Unknown error"
}
}
})Композиция Exit
// У вас есть три независимые операции, каждая возвращает Exit
// Нужно скомбинировать их результаты
interface User {
readonly id: string
readonly name: string
}
interface Order {
readonly orderId: string
readonly total: number
}
interface Shipping {
readonly address: string
readonly estimatedDays: number
}
// Результаты операций
declare const userExit: Exit.Exit<User, "UserNotFound">
declare const orderExit: Exit.Exit<Order, "OrderNotFound">
declare const shippingExit: Exit.Exit<Shipping, "ShippingUnavailable">
interface OrderSummary {
readonly userName: string
readonly orderId: string
readonly total: number
readonly shippingAddress: string
readonly estimatedDays: number
}
// TODO: Реализуйте функцию
// Должна вернуть Exit с OrderSummary если все успешны,
// или Failure с первой ошибкой
const combineResults = (
user: Exit.Exit<User, "UserNotFound">,
order: Exit.Exit<Order, "OrderNotFound">,
shipping: Exit.Exit<Shipping, "ShippingUnavailable">
): Exit.Exit<OrderSummary, "UserNotFound" | "OrderNotFound" | "ShippingUnavailable"> => {
// Ваш код
}
interface User {
readonly id: string
readonly name: string
}
interface Order {
readonly orderId: string
readonly total: number
}
interface Shipping {
readonly address: string
readonly estimatedDays: number
}
interface OrderSummary {
readonly userName: string
readonly orderId: string
readonly total: number
readonly shippingAddress: string
readonly estimatedDays: number
}
const combineResults = (
user: Exit.Exit<User, "UserNotFound">,
order: Exit.Exit<Order, "OrderNotFound">,
shipping: Exit.Exit<Shipping, "ShippingUnavailable">
): Exit.Exit<OrderSummary, "UserNotFound" | "OrderNotFound" | "ShippingUnavailable"> =>
pipe(
user,
Exit.flatMap((u) =>
pipe(
order,
Exit.flatMap((o) =>
pipe(
shipping,
Exit.map((s) => ({
userName: u.name,
orderId: o.orderId,
total: o.total,
shippingAddress: s.address,
estimatedDays: s.estimatedDays
}))
)
)
)
)
)
// Альтернатива через Exit.all и деструктуризацию
const combineResultsAlt = (
user: Exit.Exit<User, "UserNotFound">,
order: Exit.Exit<Order, "OrderNotFound">,
shipping: Exit.Exit<Shipping, "ShippingUnavailable">
): Exit.Exit<OrderSummary, "UserNotFound" | "OrderNotFound" | "ShippingUnavailable"> =>
pipe(
Exit.all([user, order, shipping] as const),
Exit.map(([u, o, s]) => ({
userName: u.name,
orderId: o.orderId,
total: o.total,
shippingAddress: s.address,
estimatedDays: s.estimatedDays
}))
)Exit в Effect контексте
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly query: string
}> {}
// Симуляция запроса к БД
declare const queryDatabase: (
query: string
) => Effect.Effect<ReadonlyArray<unknown>, DatabaseError>
// TODO: Реализуйте функцию
// Должна:
// 1. Выполнить запрос
// 2. Логировать результат (успех или ошибку)
// 3. Вернуть пустой массив при ошибке вместо проброса ошибки
const safeQuery = (
query: string
): Effect.Effect<ReadonlyArray<unknown>, never> => {
// Ваш код
}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly query: string
}> {}
const queryDatabase = (
query: string
): Effect.Effect<ReadonlyArray<unknown>, DatabaseError> =>
Effect.fail(new DatabaseError({ query }))
const safeQuery = (
query: string
): Effect.Effect<ReadonlyArray<unknown>, never> =>
Effect.gen(function* () {
const exit = yield* Effect.exit(queryDatabase(query))
return yield* Exit.match(exit, {
onSuccess: (results) =>
Effect.gen(function* () {
yield* Console.log(`✅ Query succeeded: ${query}, rows: ${results.length}`)
return results
}),
onFailure: (cause) =>
Effect.gen(function* () {
const error = Cause.failureOption(cause)
if (error._tag === "Some") {
yield* Console.error(`❌ Query failed: ${error.value.query}`)
} else {
yield* Console.error(`❌ Query failed with defect`)
}
return [] as ReadonlyArray<unknown>
})
})
})
// Альтернатива через Effect.catchAll
const safeQueryAlt = (
query: string
): Effect.Effect<ReadonlyArray<unknown>, never> =>
queryDatabase(query).pipe(
Effect.tap((results) =>
Console.log(`✅ Query succeeded: ${query}, rows: ${results.length}`)
),
Effect.catchAll((error) =>
Console.error(`❌ Query failed: ${error.query}`).pipe(
Effect.as([] as ReadonlyArray<unknown>)
)
)
)Partition Exit по типу ошибки
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
}> {}
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly table: string
}> {}
type AppError = ValidationError | NetworkError | DatabaseError
interface PartitionedExits<A> {
readonly successes: ReadonlyArray<A>
readonly validationErrors: ReadonlyArray<ValidationError>
readonly networkErrors: ReadonlyArray<NetworkError>
readonly databaseErrors: ReadonlyArray<DatabaseError>
readonly defects: ReadonlyArray<unknown>
}
// TODO: Реализуйте функцию
// Разделяет массив Exit по категориям
const partitionExits = <A>(
exits: ReadonlyArray<Exit.Exit<A, AppError>>
): PartitionedExits<A> => {
// Ваш код
}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
}> {}
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly table: string
}> {}
type AppError = ValidationError | NetworkError | DatabaseError
interface PartitionedExits<A> {
readonly successes: ReadonlyArray<A>
readonly validationErrors: ReadonlyArray<ValidationError>
readonly networkErrors: ReadonlyArray<NetworkError>
readonly databaseErrors: ReadonlyArray<DatabaseError>
readonly defects: ReadonlyArray<unknown>
}
const partitionExits = <A>(
exits: ReadonlyArray<Exit.Exit<A, AppError>>
): PartitionedExits<A> => {
const successes: A[] = []
const validationErrors: ValidationError[] = []
const networkErrors: NetworkError[] = []
const databaseErrors: DatabaseError[] = []
const defects: unknown[] = []
for (const exit of exits) {
if (Exit.isSuccess(exit)) {
successes.push(exit.value)
continue
}
// Обрабатываем Failure
const cause = exit.cause
// Собираем ожидаемые ошибки
const failures = [...Cause.failures(cause)]
for (const error of failures) {
switch (error._tag) {
case "ValidationError":
validationErrors.push(error)
break
case "NetworkError":
networkErrors.push(error)
break
case "DatabaseError":
databaseErrors.push(error)
break
}
}
// Собираем дефекты
const causeDefects = [...Cause.defects(cause)]
defects.push(...causeDefects)
}
return {
successes,
validationErrors,
networkErrors,
databaseErrors,
defects
} as const
}
// Тест
const testExits: ReadonlyArray<Exit.Exit<string, AppError>> = [
Exit.succeed("ok1"),
Exit.fail(new ValidationError({ field: "email" })),
Exit.succeed("ok2"),
Exit.fail(new NetworkError({ url: "https://api.com" })),
Exit.die(new Error("Crash!")),
Exit.fail(new DatabaseError({ table: "users" }))
]
console.log(partitionExits(testExits))Exit-based Circuit Breaker
class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{
readonly service: string
}> {}
interface CircuitBreakerState {
readonly failures: number
readonly lastFailure: number | null
readonly isOpen: boolean
}
// TODO: Реализуйте Circuit Breaker на основе Exit
// - Если 3 последовательных ошибки — открыть circuit
// - Если circuit открыт — сразу возвращать ServiceUnavailable
// - После 5 секунд — закрыть circuit и попробовать снова
const createCircuitBreaker = <A, E>(
service: string,
maxFailures: number,
resetTimeout: number
): {
readonly execute: (
effect: Effect.Effect<A, E>
) => Effect.Effect<A, E | ServiceUnavailable>
readonly getState: Effect.Effect<CircuitBreakerState>
} => {
// Ваш код
}
class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{
readonly service: string
}> {}
interface CircuitBreakerState {
readonly failures: number
readonly lastFailure: number | null
readonly isOpen: boolean
}
const createCircuitBreaker = <A, E>(
service: string,
maxFailures: number,
resetTimeout: number
) => {
const initialState: CircuitBreakerState = {
failures: 0,
lastFailure: null,
isOpen: false
}
// Создаём Ref для состояния (внутри Effect)
const makeCircuitBreaker = Effect.gen(function* () {
const stateRef = yield* Ref.make(initialState)
const execute = <A, E>(
effect: Effect.Effect<A, E>
): Effect.Effect<A, E | ServiceUnavailable> =>
Effect.gen(function* () {
const state = yield* Ref.get(stateRef)
const now = yield* Clock.currentTimeMillis
// Проверяем, нужно ли сбросить circuit
if (state.isOpen && state.lastFailure !== null) {
if (now - state.lastFailure > resetTimeout) {
yield* Ref.set(stateRef, { ...initialState })
} else {
return yield* Effect.fail(
new ServiceUnavailable({ service })
)
}
}
// Если circuit открыт — сразу ошибка
const currentState = yield* Ref.get(stateRef)
if (currentState.isOpen) {
return yield* Effect.fail(
new ServiceUnavailable({ service })
)
}
// Выполняем эффект и анализируем Exit
const exit = yield* Effect.exit(effect)
if (Exit.isSuccess(exit)) {
// Сбрасываем счётчик при успехе
yield* Ref.set(stateRef, { ...initialState })
return exit.value
}
// При ошибке увеличиваем счётчик
const newFailures = currentState.failures + 1
const shouldOpen = newFailures >= maxFailures
yield* Ref.set(stateRef, {
failures: newFailures,
lastFailure: now,
isOpen: shouldOpen
})
// Пробрасываем оригинальную ошибку
return yield* Effect.failCause(exit.cause)
})
const getState = Ref.get(stateRef)
return { execute, getState }
})
// Возвращаем "ленивую" версию, которая создаёт CB при первом использовании
// В реальном коде это было бы через Layer
return {
execute: <A, E>(effect: Effect.Effect<A, E>) =>
Effect.gen(function* () {
const cb = yield* makeCircuitBreaker
return yield* cb.execute(effect)
}),
getState: Effect.gen(function* () {
const cb = yield* makeCircuitBreaker
return yield* cb.getState
})
}
}
// Тест
const riskyOperation = Effect.gen(function* () {
if (Math.random() < 0.8) {
return yield* Effect.fail("Service down")
}
return "Success!"
})
const cb = createCircuitBreaker<string, string>("MyService", 3, 5000)
const program = Effect.gen(function* () {
for (let i = 0; i < 10; i++) {
const exit = yield* Effect.exit(cb.execute(riskyOperation))
console.log(`Attempt ${i + 1}:`, Exit.isSuccess(exit) ? "✅" : "❌")
}
})
Effect.runPromise(program)Резюме
| Аспект | Success | Failure |
|---|---|---|
| Тег | _tag: "Success" | _tag: "Failure" |
| Содержит | value: A | cause: Cause<E> |
| Создание | Exit.succeed(a) | Exit.fail(e), Exit.die(d) |
| Проверка | Exit.isSuccess(exit) | Exit.isFailure(exit) |
Ключевые выводы:
Exit<A, E>— это результат завершения Effect: либо успех со значением, либо неудача с полной информацией о причинахFailureсодержитCause<E>, а не простоE— это позволяет представить сложные сценарии ошибок- Используйте
Exit.matchдля безопасного pattern matching Exitобразует монаду — можно комбинировать черезflatMap,map,zipEffect.exitпреобразуетEffect<A, E, R>вEffect<Exit<A, E>, never, R>для анализа без проброса ошибки