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"> => {
// Ваш код
}
const fetchFromPrimary = (): Effect.Effect<string, "PrimaryError"> =>
Effect.fail("PrimaryError")
const fetchFromBackup = (): Effect.Effect<string, "BackupError"> =>
Effect.succeed("Backup data")
const fetchData = (): Effect.Effect<string, "BackupError"> =>
fetchFromPrimary().pipe(
Effect.orElse(() => fetchFromBackup())
)
Effect.runPromise(fetchData()).then(console.log) // "Backup data"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> => {
// Ваш код
}
class ConfigError extends Data.TaggedError("ConfigError")<{
readonly key: string
}> {}
const getConfig = (key: string): Effect.Effect<number, ConfigError> =>
Effect.fail(new ConfigError({ key }))
const getConfigWithDefault = (
key: string,
defaultValue: number
): Effect.Effect<number, never> =>
getConfig(key).pipe(
Effect.orElseSucceed(() => defaultValue)
)
Effect.runPromise(getConfigWithDefault("PORT", 3000)).then(console.log) // 3000Цепочка 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> => {
// Ваш код
}
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")
const withLogging = (
source: string,
effect: Effect.Effect<string, SourceError>
) =>
Console.log(`Trying ${source}...`).pipe(
Effect.zipRight(effect),
Effect.tap((data) => Console.log(`✅ Got data from ${source}`)),
Effect.tapError((e) => Console.log(`❌ ${source} failed: ${e.reason}`)),
Effect.map((data) => ({ data, source }))
)
const fetchWithLogging = (): Effect.Effect<{
data: string
source: string
}, SourceError> =>
withLogging("memory", fromMemory()).pipe(
Effect.orElse(() => withLogging("disk", fromDisk())),
Effect.orElse(() => withLogging("network", fromNetwork()))
)
Effect.runPromise(fetchWithLogging()).then(console.log)
/*
Trying memory...
❌ memory failed: Not cached
Trying disk...
❌ disk failed: File not found
Trying network...
✅ Got data from network
{ data: 'Network data', source: 'network' }
*/firstSuccessOf с таймаутом
// TODO: Реализуйте функцию, которая пробует все эффекты
// с таймаутом на каждый. Если эффект не успевает за timeout,
// переходим к следующему.
const firstSuccessWithTimeout = <A, E>(
effects: ReadonlyArray<Effect.Effect<A, E>>,
timeout: Duration.Duration
): Effect.Effect<A, E | "AllTimedOut"> => {
// Ваш код
}
const firstSuccessWithTimeout = <A, E>(
effects: ReadonlyArray<Effect.Effect<A, E>>,
timeout: Duration.Duration
): Effect.Effect<A, E | "AllTimedOut"> =>
Effect.firstSuccessOf(
effects.map((effect) =>
effect.pipe(
Effect.timeout(timeout),
Effect.mapError((option) =>
option._tag === "Some" ? "Timeout" as const : option._tag
)
)
)
).pipe(
Effect.orElseFail(() => "AllTimedOut" as const)
)
// Альтернативное решение с обработкой таймаутов по-отдельности
const firstSuccessWithTimeoutAlt = <A, E>(
effects: ReadonlyArray<Effect.Effect<A, E>>,
timeout: Duration.Duration
): Effect.Effect<A, E | "AllTimedOut"> =>
Effect.gen(function* () {
for (const effect of effects) {
const result = yield* effect.pipe(
Effect.timeout(timeout),
Effect.either
)
if (result._tag === "Right") {
return result.right
}
// Если таймаут - пробуем следующий
}
return yield* Effect.fail("AllTimedOut" as const)
})
// Тест
const slowEffect = (delay: number, value: string) =>
Effect.succeed(value).pipe(Effect.delay(delay))
const program = firstSuccessWithTimeout(
[
slowEffect(2000, "slow"), // Не успеет
slowEffect(500, "fast"), // Должен сработать
slowEffect(100, "faster") // Не дойдем до этого
],
Duration.seconds(1)
)
Effect.runPromise(program).then(console.log) // "fast"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> => {
// Ваш код
}
interface FallbackSource<A, E> {
readonly name: string
readonly weight: number
readonly effect: Effect.Effect<A, E>
}
const weightedFirstSuccess = <A, E>(
sources: ReadonlyArray<FallbackSource<A, E>>
): Effect.Effect<{
value: A
source: string
triedSources: ReadonlyArray<string>
}, E> =>
Effect.gen(function* () {
// Сортируем по весу (descending)
const sorted = pipe(
sources,
Array.sortBy(
(a, b) => b.weight - a.weight // descending
)
)
const triedSources: string[] = []
for (const { name, effect } of sorted) {
triedSources.push(name)
const result = yield* effect.pipe(
Effect.map((value) => ({ success: true as const, value })),
Effect.catchAll(() =>
Effect.succeed({ success: false as const })
)
)
if (result.success) {
return {
value: result.value,
source: name,
triedSources
}
}
}
// Если все неудачны — пробрасываем последнюю ошибку
const lastSource = sorted[sorted.length - 1]!
return yield* lastSource.effect.pipe(
Effect.map((value) => ({
value,
source: lastSource.name,
triedSources
}))
)
})
// Тест
class FetchError extends Data.TaggedError("FetchError")<{ source: string }> {}
const sources: ReadonlyArray<FallbackSource<string, FetchError>> = [
{
name: "backup",
weight: 1,
effect: Effect.succeed("backup data")
},
{
name: "primary",
weight: 10,
effect: Effect.fail(new FetchError({ source: "primary" }))
},
{
name: "cdn",
weight: 5,
effect: Effect.fail(new FetchError({ source: "cdn" }))
}
]
Effect.runPromise(weightedFirstSuccess(sources)).then(console.log)
/*
{
value: 'backup data',
source: 'backup',
triedSources: [ 'primary', 'cdn', 'backup' ]
}
*/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>>
}> => {
// Ваш код
}
interface SourceHealth {
readonly successCount: number
readonly failureCount: number
readonly lastSuccess: number | null
readonly lastFailure: number | null
}
const initialHealth: SourceHealth = {
successCount: 0,
failureCount: 0,
lastSuccess: null,
lastFailure: null
}
const calculateScore = (health: SourceHealth, now: number): number => {
const total = health.successCount + health.failureCount
if (total === 0) return 0.5 // Unknown source — neutral score
const successRate = health.successCount / total
// Recency factor — more recent activity = higher weight
const lastActivity = Math.max(
health.lastSuccess ?? 0,
health.lastFailure ?? 0
)
const ageMs = now - lastActivity
const recencyFactor = Math.exp(-ageMs / (1000 * 60 * 5)) // 5 min half-life
return successRate * (0.5 + 0.5 * recencyFactor)
}
const createAdaptiveFallback = <A, E>(
sources: ReadonlyArray<{
name: string
effect: Effect.Effect<A, E>
}>
) =>
Effect.gen(function* () {
type HealthMap = HashMap.HashMap<string, SourceHealth>
const healthRef = yield* Ref.make<HealthMap>(
HashMap.fromIterable(
sources.map(({ name }) => [name, initialHealth] as const)
)
)
const updateHealth = (
name: string,
success: boolean,
timestamp: number
): Effect.Effect<void> =>
Ref.update(healthRef, (map) => {
const current = HashMap.get(map, name)
if (current._tag === "None") return map
const updated: SourceHealth = success
? {
...current.value,
successCount: current.value.successCount + 1,
lastSuccess: timestamp
}
: {
...current.value,
failureCount: current.value.failureCount + 1,
lastFailure: timestamp
}
return HashMap.set(map, name, updated)
})
const execute: Effect.Effect<A, E> = Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis
const healthMap = yield* Ref.get(healthRef)
// Сортируем источники по health score
const sorted = pipe(
sources,
Array.sortBy((a, b) => {
const healthA = HashMap.get(healthMap, a.name)
const healthB = HashMap.get(healthMap, b.name)
const scoreA = healthA._tag === "Some"
? calculateScore(healthA.value, now)
: 0.5
const scoreB = healthB._tag === "Some"
? calculateScore(healthB.value, now)
: 0.5
return scoreB - scoreA // Descending
})
)
// Пробуем источники по порядку
for (const { name, effect } of sorted) {
const result = yield* effect.pipe(
Effect.tap(() => updateHealth(name, true, now)),
Effect.map((value) => ({ success: true as const, value })),
Effect.catchAll(() =>
updateHealth(name, false, now).pipe(
Effect.as({ success: false as const })
)
)
)
if (result.success) {
return result.value
}
}
// Все неудачны — пробрасываем последнюю ошибку
return yield* sorted[sorted.length - 1]!.effect
})
const getHealth = Ref.get(healthRef).pipe(
Effect.map((hm) => new Map(HashMap.toEntries(hm)))
)
return { execute, getHealth }
})
// Тест
class SourceError extends Data.TaggedError("SourceError")<{ source: string }> {}
const testProgram = Effect.gen(function* () {
const { execute, getHealth } = yield* createAdaptiveFallback([
{
name: "fast-unstable",
effect: Math.random() > 0.7
? Effect.succeed("fast")
: Effect.fail(new SourceError({ source: "fast" }))
},
{
name: "slow-stable",
effect: Effect.sleep("100 millis").pipe(Effect.as("slow"))
}
])
// Выполняем несколько запросов
for (let i = 0; i < 10; i++) {
const result = yield* execute.pipe(
Effect.catchAll(() => Effect.succeed("fallback"))
)
console.log(`Request ${i + 1}: ${result}`)
}
// Смотрим итоговый health
const health = yield* getHealth
console.log("\nFinal health:")
health.forEach((h, name) => {
console.log(` ${name}: ${h.successCount}/${h.successCount + h.failureCount} success`)
})
})
Effect.runPromise(testProgram)Резюме
| Функция | Описание | Тип ошибки после |
|---|---|---|
orElse | Fallback эффект | E2 (от fallback) |
orElseSucceed | Fallback значение | never |
orElseFail | Замена ошибки | E2 |
orDie | Ошибка → дефект | never |
firstSuccessOf | Первый успешный | E (последняя ошибка) |
orElseEither | С маркировкой источника | E2 |
option | Ошибка → None | never |
Ключевые принципы:
- Fallback-функции ленивы — альтернатива выполняется только при ошибке
orElseSucceed— самый простой способ задать defaultfirstSuccessOf— удобен для списка альтернативных источниковorElseEither— когда нужно знать, какой источник сработал