Effect Курс Fallbacks

Fallbacks

Декларативные способы определения альтернативного поведения при сбоях.

Теория

Fallback vs Catch: В чём разница?

Функции catch* и orElse* решают похожие задачи, но с разной семантикой:

Аспектcatch*orElse*
ФокусОбработка ошибкиАльтернативное вычисление
Доступ к ошибкеДа, в обработчикеНет (orElseSucceed) или да (orElse)
Типичный use caseЛогирование, трансформацияFallback источники, defaults
Семантика”Если ошибка, то…""Если не получилось, попробуй…”

Визуализация fallback цепочки

    Primary Effect


    ┌─────────┐
    │ Execute │
    └────┬────┘

    Success?──Yes──▶ Return value

         No


    Fallback 1

    ┌─────────┐
    │ Execute │
    └────┬────┘

    Success?──Yes──▶ Return value

         No


    Fallback 2

        ...


    Final Fallback

    ┌─────────┐
    │ Execute │
    └────┬────┘

    Success?──Yes──▶ Return value

         No


    Return final error

Ленивость fallback-ов

Важно: все fallback-функции ленивы — альтернативный эффект выполняется только если основной завершился ошибкой.

// Fallback НЕ выполнится, т.к. основной эффект успешен
const program = Effect.succeed(42).pipe(
  Effect.orElse(() => {
    console.log("This will NOT print")
    return Effect.succeed(0)
  })
)

Effect.runSync(program) // 42, без side effect

Концепция ФП

Alternative Functor

orElse реализует операцию <|> из type class Alternative:

class Applicative f => Alternative f where
  empty :: f a
  (<|>) :: f a -> f a -> f a

В Effect это выглядит как:

// empty — эффект, который всегда падает
const empty = Effect.fail("no value")

// (<|>) — orElse
const alternative = primary.pipe(Effect.orElse(() => fallback))

Связь с Monoid

orElse образует моноид на Effect:

  • Нейтральный элемент: Effect.fail(...) — эффект, который всегда передаёт управление fallback
  • Ассоциативность: (a <|> b) <|> c ≡ a <|> (b <|> c)
// Ассоциативность
const v1 = a.pipe(
  Effect.orElse(() => b),
  Effect.orElse(() => c)
)

const v2 = a.pipe(
  Effect.orElse(() => b.pipe(Effect.orElse(() => c)))
)

// v1 ≡ v2 (семантически эквивалентны)

First Success semantics

firstSuccessOf реализует семантику First Success — возвращает результат первого успешного эффекта:

// Монадическая интерпретация
firstSuccessOf([e1, e2, e3]) ≡ 
  e1.pipe(
    Effect.orElse(() => e2),
    Effect.orElse(() => e3)
  )

API Reference

orElse

Выполняет fallback эффект, если основной завершился ошибкой.

Effect.orElse<A, E, A2, E2, R2>(
  self: Effect<A, E, R>,
  that: LazyArg<Effect<A2, E2, R2>>
): Effect<A | A2, E2, R | R2>

Особенности:

  • Fallback ленивый — выполняется только при ошибке
  • Тип ошибки меняется на E2 (от fallback)
  • Возвращаемый тип — union A | A2

const primary = Effect.fail("primary failed")
const fallback = Effect.succeed("fallback value")

const program = primary.pipe(
  Effect.orElse(() => fallback)
)
// Effect<string, never, never>

Effect.runSync(program) // "fallback value"

orElseSucceed

Возвращает заданное значение при ошибке (без выполнения эффекта).

Effect.orElseSucceed<A, E, A2>(
  self: Effect<A, E, R>,
  orElse: LazyArg<A2>
): Effect<A | A2, never, R>

Особенности:

  • Самый простой fallback — просто значение
  • Канал ошибок становится never
  • Не выполняет никаких эффектов при fallback

const program = Effect.fail("error").pipe(
  Effect.orElseSucceed(() => "default")
)
// Effect<string, never, never>

Effect.runSync(program) // "default"

orElseFail

Заменяет ошибку на новую при сбое.

Effect.orElseFail<A, E, E2>(
  self: Effect<A, E, R>,
  orElse: LazyArg<E2>
): Effect<A, E2, R>

Особенности:

  • Меняет тип ошибки с E на E2
  • Не влияет на успешный результат
  • Полезно для унификации типов ошибок

class UnifiedError extends Data.TaggedError("UnifiedError")<{
  readonly source: string
}> {}

const primary = Effect.fail("some error")

const program = primary.pipe(
  Effect.orElseFail(() => new UnifiedError({ source: "primary" }))
)
// Effect<never, UnifiedError, never>

orDie

Превращает ошибку в дефект (die) — для невосстановимых ситуаций.

Effect.orDie<A, E>(
  self: Effect<A, E, R>
): Effect<A, never, R>

Особенности:

  • Канал ошибок становится never
  • Ошибка превращается в дефект (Cause.Die)
  • Используйте когда ошибка — признак бага

// Конфиг должен быть доступен — иначе это баг
const getRequiredConfig = (key: string) =>
  Effect.fail(`Config ${key} not found`).pipe(
    Effect.orDie
  )
// Effect<never, never, never> — гарантированный crash при ошибке

firstSuccessOf

Выполняет эффекты по порядку, возвращая первый успешный.

Effect.firstSuccessOf<A, E, R>(
  effects: Iterable<Effect<A, E, R>>
): Effect<A, E, R>

Особенности:

  • Выполняет эффекты последовательно
  • Останавливается на первом успехе
  • При полном провале возвращает последнюю ошибку

const sources = [
  Effect.fail("source 1 failed"),
  Effect.fail("source 2 failed"),
  Effect.succeed("source 3 succeeded"),
  Effect.succeed("source 4 (never reached)")
]

const program = Effect.firstSuccessOf(sources)

Effect.runSync(program) // "source 3 succeeded"

orElseEither

Возвращает Either, указывая какой эффект успешно выполнился.

Effect.orElseEither<A, E, A2, E2, R2>(
  self: Effect<A, E, R>,
  that: LazyArg<Effect<A2, E2, R2>>
): Effect<Either<A2, A>, E2, R | R2>

Особенности:

  • Right(a) — основной эффект успешен
  • Left(a2) — fallback успешен
  • Позволяет различить источник результата

const primary = Effect.fail("error")
const fallback = Effect.succeed("from fallback")

const program = primary.pipe(
  Effect.orElseEither(() => fallback)
)
// Effect<Either<string, never>, never, never>

Effect.runSync(program) // { _tag: "Left", left: "from fallback" }

option

Преобразует ошибку в None, успех в Some.

Effect.option<A, E>(
  self: Effect<A, E, R>
): Effect<Option<A>, never, R>

const success = Effect.succeed(42).pipe(Effect.option)
const failure = Effect.fail("error").pipe(Effect.option)

Effect.runSync(success) // { _tag: "Some", value: 42 }
Effect.runSync(failure) // { _tag: "None" }

Паттерны использования

Паттерн 1: Каскад источников данных


class CacheError extends Data.TaggedError("CacheError")<{}> {}
class DbError extends Data.TaggedError("DbError")<{}> {}
class ApiError extends Data.TaggedError("ApiError")<{}> {}

// Источники данных
const fromCache = (key: string): Effect.Effect<string, CacheError> =>
  Effect.fail(new CacheError())

const fromDb = (key: string): Effect.Effect<string, DbError> =>
  Effect.fail(new DbError())

const fromApi = (key: string): Effect.Effect<string, ApiError> =>
  Effect.succeed(`API data for ${key}`)

// Каскад: Cache → DB → API
const getData = (key: string) =>
  fromCache(key).pipe(
    Effect.orElse(() => fromDb(key)),
    Effect.orElse(() => fromApi(key))
  )
// Effect<string, ApiError, never>

Паттерн 2: Default значения


// Конфиг с default значениями
const getPort = Config.number("PORT").pipe(
  Config.withDefault(3000)
)

// Эффект с default
const fetchUserName = (userId: string): Effect.Effect<string, Error> =>
  Effect.fail(new Error("User not found"))

const getUserNameOrDefault = (userId: string) =>
  fetchUserName(userId).pipe(
    Effect.orElseSucceed(() => "Anonymous")
  )

Паттерн 3: Graceful degradation


interface FeatureFlags {
  readonly newFeature: boolean
  readonly betaAccess: boolean
}

class FeatureServiceError extends Data.TaggedError("FeatureServiceError")<{}> {}

const fetchFeatureFlags = (): Effect.Effect<FeatureFlags, FeatureServiceError> =>
  Effect.fail(new FeatureServiceError())

// Default флаги при недоступности сервиса
const defaultFlags: FeatureFlags = {
  newFeature: false,
  betaAccess: false
}

const getFeatureFlags = () =>
  fetchFeatureFlags().pipe(
    Effect.tap(() => Console.log("Feature flags loaded from service")),
    Effect.orElse(() =>
      Console.log("Feature service unavailable, using defaults").pipe(
        Effect.as(defaultFlags)
      )
    )
  )

Паттерн 4: First available resource


class ConnectionError extends Data.TaggedError("ConnectionError")<{
  readonly host: string
}> {}

const connectTo = (host: string): Effect.Effect<string, ConnectionError> =>
  Math.random() > 0.7
    ? Effect.succeed(`Connected to ${host}`)
    : Effect.fail(new ConnectionError({ host }))

// Пул серверов — подключаемся к первому доступному
const hosts = ["server1.example.com", "server2.example.com", "server3.example.com"]

const connectToCluster = () =>
  Effect.firstSuccessOf(
    hosts.map((host) => connectTo(host))
  )
// При полном провале вернёт ConnectionError от последнего сервера

Паттерн 5: Typed fallback chain


// Разные источники с разными типами данных
const fromPremiumApi = (): Effect.Effect<{ data: string; premium: true }, "premium_error"> =>
  Effect.fail("premium_error")

const fromFreeApi = (): Effect.Effect<{ data: string; premium: false }, "free_error"> =>
  Effect.succeed({ data: "free data", premium: false as const })

// orElseEither сохраняет информацию об источнике
const getData = () =>
  fromPremiumApi().pipe(
    Effect.orElseEither(() => fromFreeApi()),
    Effect.map((result) =>
      Either.isRight(result)
        ? { ...result.right, source: "premium" as const }
        : { ...result.left, source: "free" as const }
    )
  )

// Результат: { data: string, premium: boolean, source: "premium" | "free" }

Примеры

Пример 1: Multi-tier cache system


// Ошибки для разных уровней кэша
class L1CacheMiss extends Data.TaggedError("L1CacheMiss")<{ key: string }> {}
class L2CacheMiss extends Data.TaggedError("L2CacheMiss")<{ key: string }> {}
class OriginError extends Data.TaggedError("OriginError")<{ reason: string }> {}

type CacheError = L1CacheMiss | L2CacheMiss | OriginError

interface CacheEntry<T> {
  readonly value: T
  readonly source: "L1" | "L2" | "origin"
  readonly ttl: Duration.Duration
}

// Симуляция уровней кэша
const l1Cache = new Map<string, unknown>()
const l2Cache = new Map<string, unknown>()

const getFromL1 = <T>(key: string): Effect.Effect<CacheEntry<T>, L1CacheMiss> =>
  Effect.gen(function* () {
    yield* Console.log(`[L1] Looking for ${key}`)
    const value = l1Cache.get(key)
    if (value === undefined) {
      return yield* Effect.fail(new L1CacheMiss({ key }))
    }
    yield* Console.log(`[L1] HIT for ${key}`)
    return {
      value: value as T,
      source: "L1" as const,
      ttl: Duration.minutes(1)
    }
  })

const getFromL2 = <T>(key: string): Effect.Effect<CacheEntry<T>, L2CacheMiss> =>
  Effect.gen(function* () {
    yield* Console.log(`[L2] Looking for ${key}`)
    const value = l2Cache.get(key)
    if (value === undefined) {
      return yield* Effect.fail(new L2CacheMiss({ key }))
    }
    yield* Console.log(`[L2] HIT for ${key}`)
    // Также записываем в L1
    l1Cache.set(key, value)
    return {
      value: value as T,
      source: "L2" as const,
      ttl: Duration.minutes(5)
    }
  })

const getFromOrigin = <T>(
  key: string, 
  fetcher: () => Effect.Effect<T, OriginError>
): Effect.Effect<CacheEntry<T>, OriginError> =>
  Effect.gen(function* () {
    yield* Console.log(`[Origin] Fetching ${key}`)
    const value = yield* fetcher()
    // Записываем в оба кэша
    l1Cache.set(key, value)
    l2Cache.set(key, value)
    yield* Console.log(`[Origin] Fetched and cached ${key}`)
    return {
      value,
      source: "origin" as const,
      ttl: Duration.minutes(15)
    }
  })

// Универсальный getter с fallback chain
const getCached = <T>(
  key: string,
  fetcher: () => Effect.Effect<T, OriginError>
): Effect.Effect<CacheEntry<T>, OriginError> =>
  getFromL1<T>(key).pipe(
    Effect.orElse(() => getFromL2<T>(key)),
    Effect.orElse(() => getFromOrigin<T>(key, fetcher))
  )

// Использование
const fetchUserFromDb = (id: string): Effect.Effect<{ name: string }, OriginError> =>
  Effect.succeed({ name: `User ${id}` })

const program = Effect.gen(function* () {
  // Первый запрос — из origin
  const r1 = yield* getCached("user:123", () => fetchUserFromDb("123"))
  console.log("Result 1:", r1)
  
  // Второй запрос — из L1
  const r2 = yield* getCached("user:123", () => fetchUserFromDb("123"))
  console.log("Result 2:", r2)
})

Effect.runPromise(program)

Пример 2: Circuit breaker с fallback


class ServiceUnavailable extends Data.TaggedError("ServiceUnavailable")<{
  readonly service: string
}> {}

interface CircuitState {
  readonly failures: number
  readonly lastFailure: number | null
  readonly isOpen: boolean
}

const createServiceWithCircuitBreaker = <A, E>(
  serviceName: string,
  primary: Effect.Effect<A, E>,
  fallback: Effect.Effect<A, E>,
  maxFailures: number = 3,
  resetAfterMs: number = 10000
) =>
  Effect.gen(function* () {
    const stateRef = yield* Ref.make<CircuitState>({
      failures: 0,
      lastFailure: null,
      isOpen: false
    })
    
    const execute = 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 > resetAfterMs) {
          yield* Ref.set(stateRef, { failures: 0, lastFailure: null, isOpen: false })
          yield* Console.log(`[${serviceName}] Circuit CLOSED (reset)`)
        }
      }
      
      const currentState = yield* Ref.get(stateRef)
      
      // Если circuit открыт — сразу fallback
      if (currentState.isOpen) {
        yield* Console.log(`[${serviceName}] Circuit OPEN, using fallback`)
        return yield* fallback
      }
      
      // Пробуем primary
      return yield* primary.pipe(
        Effect.tap(() =>
          Ref.set(stateRef, { failures: 0, lastFailure: null, isOpen: false })
        ),
        Effect.orElse(() =>
          Effect.gen(function* () {
            const newFailures = currentState.failures + 1
            const shouldOpen = newFailures >= maxFailures
            
            yield* Ref.set(stateRef, {
              failures: newFailures,
              lastFailure: now,
              isOpen: shouldOpen
            })
            
            if (shouldOpen) {
              yield* Console.log(`[${serviceName}] Circuit OPENED after ${newFailures} failures`)
            }
            
            yield* Console.log(`[${serviceName}] Primary failed, using fallback`)
            return yield* fallback
          })
        )
      )
    })
    
    return { execute, getState: Ref.get(stateRef) }
  })

// Использование
const program = Effect.gen(function* () {
  const flaky = Effect.gen(function* () {
    if (Math.random() > 0.3) {
      return yield* Effect.fail("Service down")
    }
    return "Primary response"
  })
  
  const fallbackData = Effect.succeed("Fallback response")
  
  const { execute } = yield* createServiceWithCircuitBreaker(
    "ExternalAPI",
    flaky,
    fallbackData,
    3,
    5000
  )
  
  // Выполняем несколько запросов
  for (let i = 0; i < 10; i++) {
    const result = yield* execute
    console.log(`Request ${i + 1}:`, result)
    yield* Effect.sleep("500 millis")
  }
})

Effect.runPromise(program)

Пример 3: Feature flags с graceful degradation


interface FeatureFlags {
  readonly experimentalUI: boolean
  readonly darkMode: boolean
  readonly betaFeatures: boolean
  readonly maxItems: number
}

class FeatureServiceError extends Data.TaggedError("FeatureServiceError")<{
  readonly reason: string
}> {}

// Default значения — консервативные
const defaultFlags: FeatureFlags = {
  experimentalUI: false,
  darkMode: false,
  betaFeatures: false,
  maxItems: 10
}

// Симуляция remote сервиса
const fetchRemoteFlags = (): Effect.Effect<FeatureFlags, FeatureServiceError> =>
  Effect.gen(function* () {
    yield* Console.log("[FeatureService] Fetching remote flags...")
    
    // Симулируем нестабильный сервис
    if (Math.random() > 0.5) {
      return yield* Effect.fail(
        new FeatureServiceError({ reason: "Connection timeout" })
      )
    }
    
    yield* Console.log("[FeatureService] Remote flags loaded")
    return {
      experimentalUI: true,
      darkMode: true,
      betaFeatures: false,
      maxItems: 100
    }
  })

// Загрузка из локального кэша
const fetchCachedFlags = (): Effect.Effect<FeatureFlags, FeatureServiceError> =>
  Effect.gen(function* () {
    yield* Console.log("[FeatureService] Trying cached flags...")
    // Симуляция: кэш тоже может быть пуст
    if (Math.random() > 0.7) {
      return yield* Effect.fail(
        new FeatureServiceError({ reason: "Cache miss" })
      )
    }
    
    yield* Console.log("[FeatureService] Cached flags loaded")
    return {
      experimentalUI: false,
      darkMode: true,
      betaFeatures: false,
      maxItems: 50
    }
  })

// Составной loader с fallback chain
const getFeatureFlags = (): Effect.Effect<{
  flags: FeatureFlags
  source: "remote" | "cache" | "default"
}> =>
  fetchRemoteFlags().pipe(
    Effect.map((flags) => ({ flags, source: "remote" as const })),
    Effect.orElse(() =>
      fetchCachedFlags().pipe(
        Effect.map((flags) => ({ flags, source: "cache" as const }))
      )
    ),
    Effect.orElse(() =>
      Console.log("[FeatureService] Using default flags").pipe(
        Effect.as({ flags: defaultFlags, source: "default" as const })
      )
    )
  )

// Использование в приложении
const initApp = Effect.gen(function* () {
  const { flags, source } = yield* getFeatureFlags()
  
  yield* Console.log(`Feature flags loaded from: ${source}`)
  yield* Console.log(`Flags: ${JSON.stringify(flags, null, 2)}`)
  
  if (flags.experimentalUI) {
    yield* Console.log("🧪 Experimental UI enabled")
  }
  
  if (flags.betaFeatures) {
    yield* Console.log("🎯 Beta features enabled")
  }
  
  return flags
})

Effect.runPromise(initApp)

Пример 4: Database replica failover


class DatabaseError extends Data.TaggedError("DatabaseError")<{
  readonly host: string
  readonly reason: string
}> {}

interface DbConnection {
  readonly host: string
  readonly role: "primary" | "replica"
}

// Симуляция подключения к БД
const connectToDb = (host: string, role: "primary" | "replica"): Effect.Effect<DbConnection, DatabaseError> =>
  Effect.gen(function* () {
    yield* Console.log(`[DB] Connecting to ${role} at ${host}...`)
    
    // Симулируем случайные сбои
    const success = yield* Random.next
    if (success < 0.4) {
      return yield* Effect.fail(
        new DatabaseError({ host, reason: "Connection refused" })
      )
    }
    
    yield* Console.log(`[DB] Connected to ${role} at ${host}`)
    return { host, role }
  })

// Конфигурация реплик
const primaryHost = "db-primary.example.com"
const replicaHosts = [
  "db-replica-1.example.com",
  "db-replica-2.example.com",
  "db-replica-3.example.com"
]

// Read query — можно на любой реплике
const executeReadQuery = <T>(query: string, defaultValue: T): Effect.Effect<T> =>
  Effect.gen(function* () {
    // Пробуем реплики в случайном порядке
    const shuffled = yield* Random.shuffle(replicaHosts)
    
    const connection = yield* Effect.firstSuccessOf([
      // Сначала реплики
      ...Array.map(shuffled, (host) => connectToDb(host, "replica")),
      // В крайнем случае — primary
      connectToDb(primaryHost, "primary")
    ])
    
    yield* Console.log(`[DB] Executing query on ${connection.role}: ${query}`)
    return defaultValue // В реальности тут был бы запрос
  }).pipe(
    Effect.orElseSucceed(() => {
      console.log("[DB] All databases unavailable, returning default")
      return defaultValue
    })
  )

// Write query — только primary
const executeWriteQuery = <T>(query: string): Effect.Effect<T, DatabaseError> =>
  Effect.gen(function* () {
    const connection = yield* connectToDb(primaryHost, "primary")
    yield* Console.log(`[DB] Executing write on primary: ${query}`)
    return undefined as T
  })

// Использование
const program = Effect.gen(function* () {
  // Read — с fallback на реплики
  const users = yield* executeReadQuery("SELECT * FROM users", [] as string[])
  console.log("Users:", users)
  
  // Write — только primary, без fallback
  yield* executeWriteQuery("INSERT INTO users VALUES (...)").pipe(
    Effect.catchAll((e) => 
      Console.error(`Write failed: ${e.reason}`)
    )
  )
})

Effect.runPromise(program)

Пример 5: firstSuccessOf с диагностикой


class AttemptError extends Data.TaggedError("AttemptError")<{
  readonly source: string
  readonly reason: string
}> {}

interface AttemptResult<T> {
  readonly value: T
  readonly source: string
  readonly attemptNumber: number
  readonly failedAttempts: ReadonlyArray<{ source: string; reason: string }>
}

// firstSuccessOf с полной диагностикой
const firstSuccessWithDiagnostics = <T>(
  attempts: ReadonlyArray<{
    source: string
    effect: Effect.Effect<T, AttemptError>
  }>
): Effect.Effect<AttemptResult<T>, AttemptError> =>
  Effect.gen(function* () {
    const failedAttempts: { source: string; reason: string }[] = []
    
    for (let i = 0; i < attempts.length; i++) {
      const { source, effect } = attempts[i]!
      
      const result = yield* effect.pipe(
        Effect.map((value) => ({ success: true as const, value })),
        Effect.catchAll((error) => {
          failedAttempts.push({ source, reason: error.reason })
          return Effect.succeed({ success: false as const, error })
        })
      )
      
      if (result.success) {
        return {
          value: result.value,
          source,
          attemptNumber: i + 1,
          failedAttempts
        }
      }
    }
    
    // Все попытки неудачны
    const lastAttempt = attempts[attempts.length - 1]!
    return yield* Effect.fail(
      new AttemptError({
        source: lastAttempt.source,
        reason: `All ${attempts.length} attempts failed`
      })
    )
  })

// Использование
const fetchFromCdn = Effect.gen(function* () {
  yield* Console.log("Trying CDN...")
  return yield* Effect.fail(
    new AttemptError({ source: "CDN", reason: "Cache miss" })
  )
})

const fetchFromS3 = Effect.gen(function* () {
  yield* Console.log("Trying S3...")
  return yield* Effect.fail(
    new AttemptError({ source: "S3", reason: "Rate limited" })
  )
})

const fetchFromOrigin = Effect.gen(function* () {
  yield* Console.log("Trying Origin...")
  return "Data from origin"
})

const program = firstSuccessWithDiagnostics([
  { source: "CDN", effect: fetchFromCdn },
  { source: "S3", effect: fetchFromS3 },
  { source: "Origin", effect: fetchFromOrigin }
])

Effect.runPromise(program).then((result) => {
  console.log("\n--- Result ---")
  console.log(`Value: ${result.value}`)
  console.log(`Source: ${result.source}`)
  console.log(`Attempt #: ${result.attemptNumber}`)
  console.log(`Failed attempts:`, result.failedAttempts)
})

Упражнения

Упражнение

Простой fallback

Легко

const fetchFromPrimary = (): Effect.Effect<string, "PrimaryError"> =>
  Effect.fail("PrimaryError")

const fetchFromBackup = (): Effect.Effect<string, "BackupError"> =>
  Effect.succeed("Backup data")

// TODO: Реализуйте функцию, которая пробует primary, затем backup
const fetchData = (): Effect.Effect<string, "BackupError"> => {
  // Ваш код
}
Упражнение

Default значение

Легко

class ConfigError extends Data.TaggedError("ConfigError")<{
  readonly key: string
}> {}

const getConfig = (key: string): Effect.Effect<number, ConfigError> =>
  Effect.fail(new ConfigError({ key }))

// TODO: Реализуйте функцию, которая возвращает default при ошибке
const getConfigWithDefault = (
  key: string,
  defaultValue: number
): Effect.Effect<number, never> => {
  // Ваш код
}
Упражнение

Цепочка fallback с логированием

Средне

class SourceError extends Data.TaggedError("SourceError")<{
  readonly source: string
  readonly reason: string
}> {}

const fromMemory = (): Effect.Effect<string, SourceError> =>
  Effect.fail(new SourceError({ source: "memory", reason: "Not cached" }))

const fromDisk = (): Effect.Effect<string, SourceError> =>
  Effect.fail(new SourceError({ source: "disk", reason: "File not found" }))

const fromNetwork = (): Effect.Effect<string, SourceError> =>
  Effect.succeed("Network data")

// TODO: Реализуйте функцию, которая:
// 1. Логирует каждую попытку
// 2. Логирует ошибки
// 3. Возвращает данные из первого успешного источника
const fetchWithLogging = (): Effect.Effect<{
  data: string
  source: string
}, SourceError> => {
  // Ваш код
}
Упражнение

firstSuccessOf с таймаутом

Средне

// TODO: Реализуйте функцию, которая пробует все эффекты
// с таймаутом на каждый. Если эффект не успевает за timeout,
// переходим к следующему.

const firstSuccessWithTimeout = <A, E>(
  effects: ReadonlyArray<Effect.Effect<A, E>>,
  timeout: Duration.Duration
): Effect.Effect<A, E | "AllTimedOut"> => {
  // Ваш код
}
Упражнение

Weighted fallback с приоритетами

Сложно

interface FallbackSource<A, E> {
  readonly name: string
  readonly weight: number // Больше = выше приоритет
  readonly effect: Effect.Effect<A, E>
}

// TODO: Реализуйте функцию, которая:
// 1. Сортирует источники по весу (от большего к меньшему)
// 2. Пробует их по порядку
// 3. Возвращает результат с информацией об источнике

const weightedFirstSuccess = <A, E>(
  sources: ReadonlyArray<FallbackSource<A, E>>
): Effect.Effect<{
  value: A
  source: string
  triedSources: ReadonlyArray<string>
}, E> => {
  // Ваш код
}
Упражнение

Adaptive fallback с health tracking

Сложно

interface SourceHealth {
  readonly successCount: number
  readonly failureCount: number
  readonly lastSuccess: number | null
  readonly lastFailure: number | null
}

// TODO: Создайте адаптивную систему fallback, которая:
// 1. Отслеживает health каждого источника
// 2. Автоматически переупорядочивает источники по health score
// 3. Health score = successRate * recency factor

const createAdaptiveFallback = <A, E>(
  sources: ReadonlyArray<{
    name: string
    effect: Effect.Effect<A, E>
  }>
): Effect.Effect<{
  execute: Effect.Effect<A, E>
  getHealth: Effect.Effect<ReadonlyMap<string, SourceHealth>>
}> => {
  // Ваш код
}

Резюме

ФункцияОписаниеТип ошибки после
orElseFallback эффектE2 (от fallback)
orElseSucceedFallback значениеnever
orElseFailЗамена ошибкиE2
orDieОшибка → дефектnever
firstSuccessOfПервый успешныйE (последняя ошибка)
orElseEitherС маркировкой источникаE2
optionОшибка → Nonenever

Ключевые принципы:

  • Fallback-функции ленивы — альтернатива выполняется только при ошибке
  • orElseSucceed — самый простой способ задать default
  • firstSuccessOf — удобен для списка альтернативных источников
  • orElseEither — когда нужно знать, какой источник сработал