Effect Курс Exit: Результат выполнения Effect

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>
ОшибкаОдна ошибка ECause<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 => {
  // Ваш код
}
Упражнение

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 => {
  // Ваш код
}
Упражнение

Композиция 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"> => {
  // Ваш код
}
Упражнение

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> => {
  // Ваш код
}
Упражнение

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> => {
  // Ваш код
}
Упражнение

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>
} => {
  // Ваш код
}

Резюме

АспектSuccessFailure
Тег_tag: "Success"_tag: "Failure"
Содержитvalue: Acause: 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, zip
  • Effect.exit преобразует Effect<A, E, R> в Effect<Exit<A, E>, never, R> для анализа без проброса ошибки