Effect Курс Генераторы

Генераторы

Генераторы — это синтаксический сахар для написания последовательного асинхронного кода.

Теория

Проблема callback hell и цепочек

В функциональном программировании последовательные вычисления часто выражаются через flatMap:


// Проблема: глубокая вложенность
const program = pipe(
  getUser(userId),
  Effect.flatMap((user) =>
    pipe(
      getOrders(user.id),
      Effect.flatMap((orders) =>
        pipe(
          calculateTotal(orders),
          Effect.flatMap((total) =>
            pipe(
              applyDiscount(total, user.tier),
              Effect.map((finalPrice) => ({
                user,
                orders,
                total,
                finalPrice
              }))
            )
          )
        )
      )
    )
  )
)

Решение: генераторы

Генераторы позволяют “вытащить” значение из Effect и работать с ним как с обычной переменной:


// Чистый, читаемый код
const program = Effect.gen(function* () {
  const user = yield* getUser(userId)
  const orders = yield* getOrders(user.id)
  const total = yield* calculateTotal(orders)
  const finalPrice = yield* applyDiscount(total, user.tier)
  
  return { user, orders, total, finalPrice }
})

Как это работает

┌─────────────────────────────────────────────────────────────────┐
│                    Effect.gen(function* () {                    │
│                                                                 │
│  yield* effect1  ──►  Приостановка, выполнение effect1          │
│         │                                                       │
│         ▼                                                       │
│    value1 = A    ◄──  Возобновление с результатом               │
│         │                                                       │
│  yield* effect2  ──►  Приостановка, выполнение effect2          │
│         │                                                       │
│         ▼                                                       │
│    value2 = B    ◄──  Возобновление с результатом               │
│         │                                                       │
│    return C      ──►  Финальный результат                       │
│                                                                 │
│  })                                                             │
│                                                                 │
│  Результат: Effect<C, E1 | E2, R1 | R2>                         │
│             (объединение всех ошибок и зависимостей)            │
└─────────────────────────────────────────────────────────────────┘

Концепция ФП

Do-notation в Haskell

Effect.gen — это реализация do-notation из Haskell. В Haskell:

-- Haskell do-notation
program :: IO (User, [Order], Price)
program = do
  user   <- getUser userId
  orders <- getOrders (userId user)
  total  <- calculateTotal orders
  price  <- applyDiscount total (tier user)
  return (user, orders, price)

В Effect:

// TypeScript с Effect.gen
const program: Effect.Effect<
  { user: User; orders: Order[]; price: Price },
  GetUserError | GetOrdersError | CalculateError | DiscountError,
  UserService | OrderService
> = Effect.gen(function* () {
  const user = yield* getUser(userId)
  const orders = yield* getOrders(user.id)
  const total = yield* calculateTotal(orders)
  const price = yield* applyDiscount(total, user.tier)
  return { user, orders, price }
})

Законы монады через генераторы


// Left Identity: return a >>= f ≡ f(a)
const leftIdentity = <A, B, E>(
  a: A, 
  f: (a: A) => Effect.Effect<B, E>
) => {
  // Эти два выражения эквивалентны:
  const via_gen = Effect.gen(function* () {
    const x = yield* Effect.succeed(a)
    return yield* f(x)
  })
  
  const direct = f(a)
  
  return true // via_gen ≡ direct
}

// Right Identity: m >>= return ≡ m
const rightIdentity = <A, E>(m: Effect.Effect<A, E>) => {
  // Эти два выражения эквивалентны:
  const via_gen = Effect.gen(function* () {
    const x = yield* m
    return x
  })
  
  const direct = m
  
  return true // via_gen ≡ direct
}

// Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g)
const associativity = <A, B, C, E1, E2>(
  m: Effect.Effect<A, E1>,
  f: (a: A) => Effect.Effect<B, E1>,
  g: (b: B) => Effect.Effect<C, E2>
) => {
  // Эти два выражения эквивалентны:
  const leftAssoc = Effect.gen(function* () {
    const a = yield* m
    const b = yield* f(a)
    return yield* g(b)
  })
  
  const rightAssoc = Effect.gen(function* () {
    const a = yield* m
    return yield* Effect.gen(function* () {
      const b = yield* f(a)
      return yield* g(b)
    })
  })
  
  return true // leftAssoc ≡ rightAssoc
}

Effect.gen — основы

Синтаксис


// ─────────────────────────────────────────────────────────────────
// Базовый синтаксис Effect.gen
// ─────────────────────────────────────────────────────────────────

// Вариант 1: function* () { ... }
const program1 = Effect.gen(function* () {
  const a = yield* Effect.succeed(1)
  const b = yield* Effect.succeed(2)
  return a + b
})
//    ^? Effect<number, never, never>

// Вариант 2: с this для доступа к контексту (редко используется)
const program2 = Effect.gen({ context: "myContext" }, function* () {
  const a = yield* Effect.succeed(1)
  return a
})

Типизация


// TypeScript автоматически выводит типы
const inferred = Effect.gen(function* () {
  const n = yield* Effect.succeed(42)
  //    ^? number
  const s = yield* Effect.succeed("hello")
  //    ^? string
  return { n, s }
})
//    ^? Effect<{ n: number; s: string }, never, never>

// Можно явно указать тип возвращаемого значения
const explicit = Effect.gen(function* (): Generator<
  Effect.Effect<unknown, Error>,
  { result: number },
  unknown
> {
  const n = yield* Effect.succeed(42)
  return { result: n }
})

// Или через as const / satisfies
const typed = Effect.gen(function* () {
  const n = yield* Effect.succeed(42)
  return { result: n } as const
}) satisfies Effect.Effect<{ readonly result: 42 }, never, never>

Важно: yield* vs yield


// ✅ ПРАВИЛЬНО: yield* (yield star)
const correct = Effect.gen(function* () {
  const value = yield* Effect.succeed(42)
  //            ^^^^^^ yield* извлекает значение из Effect
  return value
})

// ❌ НЕПРАВИЛЬНО: yield (без звёздочки)
const incorrect = Effect.gen(function* () {
  // @ts-expect-error
  const value = yield Effect.succeed(42)
  // yield без * НЕ извлекает значение, а возвращает Effect
  return value
})

// yield* работает ТОЛЬКО с Effect (или другими "yieldable" типами)
const mixedTypes = Effect.gen(function* () {
  // ✅ Effect
  const a = yield* Effect.succeed(1)
  
  // ✅ Option (если есть адаптер)
  // const b = yield* Option.some(2)
  
  // ❌ Обычные значения нельзя yield*
  // const c = yield* 3 // Ошибка!
  
  return a
})

Effect.fn — создание функций с генераторами

Зачем нужен Effect.fn

Effect.fn — это специализированный конструктор для создания функций, использующих генераторный синтаксис. Он предоставляет два ключевых преимущества:

  1. Улучшенные stack traces — при ошибке вы видите точное место определения и вызова функции
  2. Автоматическое создание span’ов — для трассировки и observability
┌─────────────────────────────────────────────────────────────────┐
│                    Сравнение подходов                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Effect.gen + обычная функция:                                  │
│  ─────────────────────────────                                  │
│  const myFunc = (n: number) =>                                  │
│    Effect.gen(function* () {                                    │
│      yield* Effect.fail(new Error("Boom"))                      │
│    })                                                           │
│                                                                 │
│  Stack trace: "Error: Boom"                                     │
│              (без информации о месте вызова)                    │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  Effect.fn:                                                     │
│  ──────────                                                     │
│  const myFunc = Effect.fn("myFunc")(function* (n: number) {     │
│    yield* Effect.fail(new Error("Boom"))                        │
│  })                                                             │
│                                                                 │
│  Stack trace: "Error: Boom                                      │
│      at <anonymous> (index.ts:3:22)  ← Место ошибки             │
│      at myFunc (index.ts:1:23)       ← Определение функции      │
│      at myFunc (index.ts:7:16)"      ← Место вызова             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Базовый синтаксис Effect.fn


// ─────────────────────────────────────────────────────────────────
// Вариант 1: Без имени span'а (только улучшенные stack traces)
// ─────────────────────────────────────────────────────────────────
const divide = Effect.fn(function* (a: number, b: number) {
  if (b === 0) {
    yield* Effect.fail(new Error("Division by zero"))
  }
  return a / b
})
//    ^? (a: number, b: number) => Effect<number, Error, never>

// ─────────────────────────────────────────────────────────────────
// Вариант 2: С именем span'а (stack traces + tracing)
// ─────────────────────────────────────────────────────────────────
const divideTraced = Effect.fn("divide")(function* (a: number, b: number) {
  yield* Effect.annotateCurrentSpan("a", a)
  yield* Effect.annotateCurrentSpan("b", b)
  
  if (b === 0) {
    yield* Effect.fail(new Error("Division by zero"))
  }
  return a / b
})

// ─────────────────────────────────────────────────────────────────
// Использование
// ─────────────────────────────────────────────────────────────────
const program = Effect.gen(function* () {
  const result = yield* divide(10, 2)
  //    ^? number
  return result
})

Effect.runPromise(program).then(console.log)
// Output: 5

Дженерики в Effect.fn


// ─────────────────────────────────────────────────────────────────
// Effect.fn с generic параметрами
// ─────────────────────────────────────────────────────────────────
const parseJson = Effect.fn("parseJson")(function* <T>(
  json: string,
  schema: Schema.Schema<T>
) {
  const parsed = yield* Schema.decode(Schema.parseJson(schema))(json)
  return parsed
})
//    ^? <T>(json: string, schema: Schema<T>) => Effect<T, ParseError, never>

// ─────────────────────────────────────────────────────────────────
// Использование с конкретными типами
// ─────────────────────────────────────────────────────────────────
const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
})

type User = Schema.Schema.Type<typeof UserSchema>

const program = Effect.gen(function* () {
  const user = yield* parseJson<User>(
    '{"id": 1, "name": "Alice", "email": "alice@example.com"}',
    UserSchema
  )
  //    ^? User
  
  return user
})

Effect.fn как pipe-функция

Effect.fn также может работать как pipe, позволяя добавить трансформации к результату:


// ─────────────────────────────────────────────────────────────────
// Effect.fn с pipeline — добавляем delay после выполнения
// ─────────────────────────────────────────────────────────────────
const slowFetch = Effect.fn(
  // Первый аргумент — генератор-функция
  function* (url: string) {
    yield* Effect.log(`Fetching: ${url}`)
    return { data: "response" }
  },
  // Второй аргумент — трансформация результата (effect, ...args)
  (effect, url) => Effect.delay(effect, Duration.seconds(1))
)

// ─────────────────────────────────────────────────────────────────
// Более сложный пример с retry и timeout
// ─────────────────────────────────────────────────────────────────
const resilientFetch = Effect.fn(
  "resilientFetch",
)(
  function* (url: string, retries: number) {
    yield* Effect.log(`Attempting fetch: ${url}`)
    
    // Имитация возможной ошибки
    const random = yield* Effect.sync(() => Math.random())
    if (random < 0.5) {
      yield* Effect.fail(new Error("Network error"))
    }
    
    return { url, data: "success" }
  },
  // Pipeline: добавляем retry и timeout
  (effect, url, retries) =>
    effect.pipe(
      Effect.retry({ times: retries }),
      Effect.timeout(Duration.seconds(5)),
      Effect.tap(() => Effect.log(`Successfully fetched: ${url}`))
    )
)

const program = Effect.gen(function* () {
  const result = yield* resilientFetch("https://api.example.com", 3)
  return result
})

Effect.fnUntraced — без трассировки

Effect.fnUntraced идентичен Effect.fn, но не добавляет трассировку. Используйте его когда:

  • Функция вызывается очень часто (hot path)
  • Трассировка не нужна (внутренние утилиты)
  • Нужна максимальная производительность

// ─────────────────────────────────────────────────────────────────
// Effect.fnUntraced — без overhead на трассировку
// ─────────────────────────────────────────────────────────────────
const fastAdd = Effect.fnUntraced(function* (a: number, b: number) {
  return a + b
})

// ─────────────────────────────────────────────────────────────────
// Сравнение: traced vs untraced
// ─────────────────────────────────────────────────────────────────

// С трассировкой — для бизнес-логики и внешних API
const processOrder = Effect.fn("processOrder")(function* (orderId: string) {
  yield* Effect.annotateCurrentSpan("orderId", orderId)
  yield* Effect.log(`Processing order: ${orderId}`)
  // ... бизнес-логика
  return { status: "processed" }
})

// Без трассировки — для внутренних hot-path утилит
const calculateHash = Effect.fnUntraced(function* (data: string) {
  // Вызывается миллионы раз, трассировка не нужна
  let hash = 0
  for (let i = 0; i < data.length; i++) {
    hash = ((hash << 5) - hash + data.charCodeAt(i)) | 0
  }
  return hash
})

// ─────────────────────────────────────────────────────────────────
// Когда что использовать
// ─────────────────────────────────────────────────────────────────
// 
// Effect.fn("name")     → Публичные API, бизнес-логика, handlers
// Effect.fn             → Внутренние функции, нужны stack traces
// Effect.fnUntraced     → Hot paths, утилиты, максимальная скорость

Effect.fn в сервисах

Effect.fn особенно полезен при создании сервисов:


// ─────────────────────────────────────────────────────────────────
// Определение сервиса с Effect.fn
// ─────────────────────────────────────────────────────────────────
class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User | null, DatabaseError>
    readonly save: (user: User) => Effect.Effect<User, DatabaseError>
    readonly delete: (id: string) => Effect.Effect<void, DatabaseError>
  }
>() {}

interface User {
  readonly id: string
  readonly name: string
  readonly email: string
}

class DatabaseError extends Error {
  readonly _tag = "DatabaseError"
}

// ─────────────────────────────────────────────────────────────────
// Реализация с Effect.fn для каждого метода
// ─────────────────────────────────────────────────────────────────
const UserRepositoryLive = Layer.succeed(
  UserRepository,
  {
    findById: Effect.fn("UserRepository.findById")(function* (id: string) {
      yield* Effect.annotateCurrentSpan("userId", id)
      yield* Effect.log(`Finding user: ${id}`)
      
      // Имитация запроса к БД
      yield* Effect.sleep("100 millis")
      
      return id === "1" 
        ? { id: "1", name: "Alice", email: "alice@example.com" }
        : null
    }),
    
    save: Effect.fn("UserRepository.save")(function* (user: User) {
      yield* Effect.annotateCurrentSpan("userId", user.id)
      yield* Effect.annotateCurrentSpan("userEmail", user.email)
      yield* Effect.log(`Saving user: ${user.id}`)
      
      yield* Effect.sleep("150 millis")
      
      return user
    }),
    
    delete: Effect.fn("UserRepository.delete")(function* (id: string) {
      yield* Effect.annotateCurrentSpan("userId", id)
      yield* Effect.log(`Deleting user: ${id}`)
      
      yield* Effect.sleep("100 millis")
    })
  }
)

// ─────────────────────────────────────────────────────────────────
// Использование сервиса
// ─────────────────────────────────────────────────────────────────
const program = Effect.gen(function* () {
  const repo = yield* UserRepository
  
  const user = yield* repo.findById("1")
  if (user) {
    yield* Effect.log(`Found: ${user.name}`)
  }
  
  const newUser = yield* repo.save({
    id: "2",
    name: "Bob",
    email: "bob@example.com"
  })
  
  return newUser
})

const runnable = program.pipe(Effect.provide(UserRepositoryLive))

Effect.runPromise(runnable).then(console.log)

Экспорт span’ов для Observability

При использовании Effect.fn("name") автоматически создаются span’ы, которые можно экспортировать:

  ConsoleSpanExporter,
  BatchSpanProcessor
} from "@opentelemetry/sdk-trace-base"

// Функция с трассировкой
const processPayment = Effect.fn("processPayment")(
  function* (amount: number, currency: string) {
    yield* Effect.annotateCurrentSpan("amount", amount)
    yield* Effect.annotateCurrentSpan("currency", currency)
    
    yield* Effect.log(`Processing payment: ${amount} ${currency}`)
    yield* Effect.sleep("200 millis")
    
    // Имитация ошибки для демонстрации
    if (amount > 10000) {
      yield* Effect.fail(new Error("Amount exceeds limit"))
    }
    
    return { transactionId: `txn-${Date.now()}`, amount, currency }
  }
)

// Настройка OpenTelemetry
const NodeSdkLive = NodeSdk.layer(() => ({
  resource: { serviceName: "payment-service" },
  spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter())
}))

const program = processPayment(100, "USD")

// Запуск с экспортом span'ов
Effect.runFork(
  program.pipe(
    Effect.provide(NodeSdkLive),
    Effect.catchAllCause(Effect.logError)
  )
)

/*
Output в консоли будет содержать span с:
- name: "processPayment"
- attributes: { amount: 100, currency: "USD" }
- status: success/error
- duration
- stack trace при ошибке
*/

Таблица сравнения

ПодходТрассировкаStack tracesКогда использовать
Effect.genБазовыеInline логика, простые computations
Effect.fn✅ УлучшенныеВнутренние функции, нужны хорошие stack traces
Effect.fn("name")✅ Span✅ УлучшенныеПубличные API, бизнес-логика, сервисы
Effect.fnUntracedHot paths, утилиты, максимальная производительность

yield* — извлечение значений

Извлечение успешного значения


const program = Effect.gen(function* () {
  // yield* "разворачивает" Effect и даёт доступ к значению
  const user = yield* Effect.succeed({ id: 1, name: "Alice" })
  //    ^? { id: number; name: string }
  
  // Можно работать с user как с обычной переменной
  const greeting = `Hello, ${user.name}!`
  
  // Можно делать yield* несколько раз
  const timestamp = yield* Effect.succeed(Date.now())
  
  return { greeting, timestamp }
})

Обработка ошибок внутри генератора


class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly field: string, readonly message: string) {}
}

class NetworkError {
  readonly _tag = "NetworkError"
  constructor(readonly code: number) {}
}

const validateInput = (input: string): Effect.Effect<string, ValidationError> =>
  input.length > 0
    ? Effect.succeed(input.trim())
    : Effect.fail(new ValidationError("input", "Cannot be empty"))

const fetchData = (query: string): Effect.Effect<string, NetworkError> =>
  Effect.succeed(`Data for: ${query}`)

// Ошибки автоматически накапливаются в union
const program = Effect.gen(function* () {
  const validated = yield* validateInput("test")
  //    ^? string
  // Если validateInput вернёт fail, генератор прервётся здесь
  
  const data = yield* fetchData(validated)
  //    ^? string
  // Если fetchData вернёт fail, генератор прервётся здесь
  
  return data
})
//    ^? Effect<string, ValidationError | NetworkError, never>

// Обработка ошибок
const handled = pipe(
  program,
  Effect.catchTag("ValidationError", (e) =>
    Effect.succeed(`Validation failed: ${e.message}`)
  ),
  Effect.catchTag("NetworkError", (e) =>
    Effect.succeed(`Network error: ${e.code}`)
  )
)

Условное выполнение


interface User {
  readonly id: string
  readonly name: string
  readonly email: string
  readonly premiumUntil?: Date
}

const getUser = (id: string): Effect.Effect<User, Error> =>
  Effect.succeed({ id, name: "Alice", email: "alice@example.com" })

const getPremiumFeatures = (user: User): Effect.Effect<string[]> =>
  Effect.succeed(["feature1", "feature2"])

// Условная логика внутри генератора
const program = Effect.gen(function* () {
  const user = yield* getUser("123")
  
  // Обычный if/else
  if (user.premiumUntil && user.premiumUntil > new Date()) {
    const features = yield* getPremiumFeatures(user)
    return { user, features, premium: true as const }
  }
  
  return { user, features: [] as string[], premium: false as const }
})

// Early return с yield*
const programWithEarlyReturn = Effect.gen(function* () {
  const user = yield* getUser("123")
  
  // Если не премиум, сразу возвращаем
  if (!user.premiumUntil || user.premiumUntil <= new Date()) {
    return { user, features: [], premium: false as const }
  }
  
  // Этот код выполнится только для премиум пользователей
  const features = yield* getPremiumFeatures(user)
  return { user, features, premium: true as const }
})

Циклы внутри генератора


interface Item {
  readonly id: string
  readonly name: string
}

const fetchItem = (id: string): Effect.Effect<Item, Error> =>
  Effect.succeed({ id, name: `Item ${id}` })

// ❌ НЕ РАБОТАЕТ: yield* внутри обычного цикла
const badLoop = Effect.gen(function* () {
  const ids = ["1", "2", "3"]
  const items: Item[] = []
  
  // Это НЕ будет работать как ожидается!
  // for (const id of ids) {
  //   const item = yield* fetchItem(id) // Проблема!
  //   items.push(item)
  // }
  
  return items
})

// ✅ ПРАВИЛЬНО: используем Effect.forEach или Effect.all
const goodLoop = Effect.gen(function* () {
  const ids = ["1", "2", "3"] as const
  
  // Последовательное выполнение
  const itemsSequential = yield* Effect.forEach(
    ids,
    (id) => fetchItem(id),
    { concurrency: 1 }
  )
  
  // Параллельное выполнение
  const itemsParallel = yield* Effect.forEach(
    ids,
    (id) => fetchItem(id),
    { concurrency: "unbounded" }
  )
  
  return { sequential: itemsSequential, parallel: itemsParallel }
})

// ✅ ПРАВИЛЬНО: Effect.all для массива эффектов
const withAll = Effect.gen(function* () {
  const effects = [
    fetchItem("1"),
    fetchItem("2"),
    fetchItem("3")
  ] as const
  
  const items = yield* Effect.all(effects)
  return items
})

// ✅ ПРАВИЛЬНО: рекурсия для сложных случаев
const processWithRetry = (
  ids: ReadonlyArray<string>,
  maxRetries: number
): Effect.Effect<Item[], Error> =>
  Effect.gen(function* () {
    const results: Item[] = []
    
    for (const id of ids) {
      // Каждый элемент в отдельном генераторе
      const item = yield* pipe(
        fetchItem(id),
        Effect.retry({ times: maxRetries })
      )
      results.push(item)
    }
    
    return results
  })

Композиция типов

Автоматическое объединение ошибок


class DatabaseError {
  readonly _tag = "DatabaseError"
  constructor(readonly query: string) {}
}

class ValidationError {
  readonly _tag = "ValidationError"  
  constructor(readonly field: string) {}
}

class AuthError {
  readonly _tag = "AuthError"
  constructor(readonly reason: string) {}
}

// Функции с разными типами ошибок
const validateInput = (x: string): Effect.Effect<string, ValidationError> =>
  Effect.succeed(x)

const authenticate = (token: string): Effect.Effect<string, AuthError> =>
  Effect.succeed("user-123")

const queryDatabase = (userId: string): Effect.Effect<object, DatabaseError> =>
  Effect.succeed({ data: "result" })

// Типы ошибок автоматически объединяются
const program = Effect.gen(function* () {
  const input = yield* validateInput("data")
  //                   ^? Effect<string, ValidationError>
  
  const userId = yield* authenticate("token")
  //                    ^? Effect<string, AuthError>
  
  const result = yield* queryDatabase(userId)
  //                    ^? Effect<object, DatabaseError>
  
  return { input, userId, result }
})
//    ^? Effect<
//         { input: string; userId: string; result: object },
//         ValidationError | AuthError | DatabaseError,  // ← Union!
//         never
//       >

Автоматическое объединение зависимостей


// Определяем сервисы
class Logger extends Context.Tag("Logger")<
  Logger,
  { readonly log: (msg: string) => Effect.Effect<void> }
>() {}

class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>() {}

class Cache extends Context.Tag("Cache")<
  Cache,
  { readonly get: (key: string) => Effect.Effect<unknown> }
>() {}

// Функции с разными зависимостями
const logMessage = (msg: string): Effect.Effect<void, never, Logger> =>
  Effect.gen(function* () {
    const logger = yield* Logger
    yield* logger.log(msg)
  })

const fetchFromDb = (id: string): Effect.Effect<unknown, never, Database> =>
  Effect.gen(function* () {
    const db = yield* Database
    return yield* db.query(`SELECT * FROM users WHERE id = '${id}'`)
  })

const getFromCache = (key: string): Effect.Effect<unknown, never, Cache> =>
  Effect.gen(function* () {
    const cache = yield* Cache
    return yield* cache.get(key)
  })

// Зависимости автоматически объединяются
const program = Effect.gen(function* () {
  yield* logMessage("Starting...")
  //     ^? Effect<void, never, Logger>
  
  const cached = yield* getFromCache("user:123")
  //             ^? Effect<unknown, never, Cache>
  
  const fresh = yield* fetchFromDb("123")
  //            ^? Effect<unknown, never, Database>
  
  yield* logMessage("Done!")
  
  return { cached, fresh }
})
//    ^? Effect<
//         { cached: unknown; fresh: unknown },
//         never,
//         Logger | Database | Cache  // ← Union всех зависимостей!
//       >

Сужение типов


class NetworkError {
  readonly _tag = "NetworkError"
  constructor(readonly code: number) {}
}

class ParseError {
  readonly _tag = "ParseError"
  constructor(readonly input: string) {}
}

const fetchData = (): Effect.Effect<string, NetworkError> =>
  Effect.succeed('{"data": "value"}')

const parseJson = (s: string): Effect.Effect<object, ParseError> =>
  Effect.try({
    try: () => JSON.parse(s),
    catch: () => new ParseError(s)
  })

// Полный тип ошибки
const fullProgram = Effect.gen(function* () {
  const raw = yield* fetchData()
  const parsed = yield* parseJson(raw)
  return parsed
})
//    ^? Effect<object, NetworkError | ParseError, never>

// Обработка части ошибок сужает тип
const partiallyHandled = Effect.gen(function* () {
  const raw = yield* pipe(
    fetchData(),
    Effect.catchTag("NetworkError", () => Effect.succeed("{}"))
  )
  // Теперь NetworkError обработан
  
  const parsed = yield* parseJson(raw)
  return parsed
})
//    ^? Effect<object, ParseError, never>  // ← Только ParseError!

// Полная обработка убирает ошибки
const fullyHandled = Effect.gen(function* () {
  const raw = yield* pipe(
    fetchData(),
    Effect.catchTag("NetworkError", () => Effect.succeed("{}"))
  )
  
  const parsed = yield* pipe(
    parseJson(raw),
    Effect.catchTag("ParseError", () => Effect.succeed({}))
  )
  
  return parsed
})
//    ^? Effect<object, never, never>  // ← Ошибок нет!

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

Паттерн: Последовательные зависимые операции


interface User {
  readonly id: string
  readonly teamId: string
}

interface Team {
  readonly id: string
  readonly name: string
  readonly ownerId: string
}

interface Permission {
  readonly userId: string
  readonly resource: string
  readonly actions: ReadonlyArray<string>
}

const getUser = (id: string): Effect.Effect<User, Error> =>
  Effect.succeed({ id, teamId: "team-1" })

const getTeam = (id: string): Effect.Effect<Team, Error> =>
  Effect.succeed({ id, name: "Engineering", ownerId: "owner-1" })

const getPermissions = (userId: string, teamId: string): Effect.Effect<Permission[], Error> =>
  Effect.succeed([{ userId, resource: "repo", actions: ["read", "write"] }])

// Зависимые операции — идеальный кейс для генераторов
const getUserContext = (userId: string) =>
  Effect.gen(function* () {
    // 1. Получаем пользователя
    const user = yield* getUser(userId)
    
    // 2. Используем user.teamId для получения команды
    const team = yield* getTeam(user.teamId)
    
    // 3. Используем оба значения для получения прав
    const permissions = yield* getPermissions(user.id, team.id)
    
    // 4. Возвращаем полный контекст
    return {
      user,
      team,
      permissions,
      isTeamOwner: team.ownerId === user.id
    } as const
  })

Паттерн: Валидация с накоплением


interface FormData {
  readonly name: string
  readonly email: string
  readonly age: string
}

interface ValidatedData {
  readonly name: string
  readonly email: string
  readonly age: number
}

class FieldError {
  constructor(
    readonly field: string,
    readonly message: string
  ) {}
}

const validateName = (name: string): Effect.Effect<string, FieldError> =>
  name.trim().length >= 2
    ? Effect.succeed(name.trim())
    : Effect.fail(new FieldError("name", "Name must be at least 2 characters"))

const validateEmail = (email: string): Effect.Effect<string, FieldError> =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
    ? Effect.succeed(email.toLowerCase())
    : Effect.fail(new FieldError("email", "Invalid email format"))

const validateAge = (age: string): Effect.Effect<number, FieldError> => {
  const num = parseInt(age, 10)
  return !isNaN(num) && num >= 0 && num <= 150
    ? Effect.succeed(num)
    : Effect.fail(new FieldError("age", "Age must be a number between 0 and 150"))
}

// Валидация с накоплением всех ошибок
const validateForm = (data: FormData): Effect.Effect<ValidatedData, FieldError[]> =>
  Effect.gen(function* () {
    // Выполняем все валидации параллельно и собираем результаты
    const results = yield* Effect.all(
      [
        pipe(validateName(data.name), Effect.either),
        pipe(validateEmail(data.email), Effect.either),
        pipe(validateAge(data.age), Effect.either)
      ],
      { concurrency: "unbounded" }
    )
    
    const [nameResult, emailResult, ageResult] = results
    
    // Собираем ошибки
    const errors: FieldError[] = []
    if (Either.isLeft(nameResult)) errors.push(nameResult.left)
    if (Either.isLeft(emailResult)) errors.push(emailResult.left)
    if (Either.isLeft(ageResult)) errors.push(ageResult.left)
    
    if (errors.length > 0) {
      yield* Effect.fail(errors)
    }
    
    // Если ошибок нет, все результаты — Right
    return {
      name: (nameResult as Either.Right<string, FieldError>).right,
      email: (emailResult as Either.Right<string, FieldError>).right,
      age: (ageResult as Either.Right<number, FieldError>).right
    }
  })

Паттерн: Resource acquisition


interface Connection {
  readonly id: string
  readonly query: (sql: string) => Effect.Effect<unknown>
  readonly close: () => Effect.Effect<void>
}

const acquireConnection = (): Effect.Effect<Connection> =>
  Effect.gen(function* () {
    yield* Effect.log("Acquiring connection...")
    return {
      id: `conn-${Date.now()}`,
      query: (sql) => Effect.succeed({ rows: [] }),
      close: () => Effect.log("Connection closed")
    }
  })

const releaseConnection = (conn: Connection): Effect.Effect<void> =>
  conn.close()

// Используем Effect.acquireRelease для гарантированного освобождения
const withConnection = <A, E>(
  use: (conn: Connection) => Effect.Effect<A, E>
): Effect.Effect<A, E, Scope.Scope> =>
  Effect.gen(function* () {
    const conn = yield* Effect.acquireRelease(
      acquireConnection(),
      (conn) => releaseConnection(conn).pipe(Effect.orDie)
    )
    
    return yield* use(conn)
  })

// Использование
const program = Effect.gen(function* () {
  yield* Effect.log("Starting transaction...")
  
  const result = yield* withConnection((conn) =>
    Effect.gen(function* () {
      yield* Effect.log(`Using connection ${conn.id}`)
      const users = yield* conn.query("SELECT * FROM users")
      const orders = yield* conn.query("SELECT * FROM orders")
      return { users, orders }
    })
  )
  
  yield* Effect.log("Transaction complete")
  return result
})

// Запуск со Scope
const runnable = Effect.scoped(program)

Паттерн: Retry с логированием


class ApiError {
  readonly _tag = "ApiError"
  constructor(
    readonly status: number,
    readonly message: string
  ) {}
}

const callApi = (endpoint: string): Effect.Effect<object, ApiError> =>
  Effect.gen(function* () {
    yield* Effect.log(`Calling ${endpoint}...`)
    // Симуляция случайного сбоя
    if (Math.random() < 0.7) {
      yield* Effect.fail(new ApiError(503, "Service unavailable"))
    }
    return { data: "success" }
  })

// Retry с логированием каждой попытки
const callApiWithRetry = (endpoint: string) =>
  Effect.gen(function* () {
    let attempt = 0
    
    const result = yield* pipe(
      Effect.gen(function* () {
        attempt++
        yield* Effect.log(`Attempt ${attempt}`)
        return yield* callApi(endpoint)
      }),
      Effect.retry(
        Schedule.exponential(Duration.millis(100)).pipe(
          Schedule.compose(Schedule.recurs(3)),
          // Логируем между попытками
          Schedule.tapOutput((duration) =>
            Effect.log(`Retrying after ${Duration.toMillis(duration)}ms...`)
          )
        )
      ),
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`All ${attempt} attempts failed: ${error.message}`)
          return { data: "fallback", error: error.message }
        })
      )
    )
    
    yield* Effect.log(`Final result after ${attempt} attempt(s)`)
    return result
  })

Сравнение с pipe

Когда использовать pipe


// ✅ pipe хорош для простых линейных трансформаций
const withPipe = pipe(
  Effect.succeed(10),
  Effect.map((n) => n * 2),
  Effect.map((n) => n.toString()),
  Effect.map((s) => `Result: ${s}`)
)

// ✅ pipe хорош для одиночных операций
const singleOp = pipe(
  someEffect,
  Effect.timeout(Duration.seconds(5)),
  Effect.retry(Schedule.recurs(3))
)

// ✅ pipe хорош когда не нужны промежуточные значения
const noIntermediates = pipe(
  fetchUser(id),
  Effect.flatMap(getOrders),
  Effect.flatMap(calculateTotal)
)

Когда использовать Effect.gen


// ✅ gen хорош для зависимых операций с промежуточными значениями
const withGen = Effect.gen(function* () {
  const user = yield* fetchUser(id)
  const orders = yield* getOrders(user.id)
  const total = yield* calculateTotal(orders)
  
  // Нужны все три значения для результата
  return {
    user,
    orderCount: orders.length,
    total,
    averageOrderValue: orders.length > 0 ? total / orders.length : 0
  }
})

// ✅ gen хорош для условной логики
const conditional = Effect.gen(function* () {
  const user = yield* getUser(id)
  
  if (user.role === "admin") {
    const adminData = yield* getAdminDashboard(user.id)
    return { type: "admin" as const, data: adminData }
  }
  
  const userData = yield* getUserDashboard(user.id)
  return { type: "user" as const, data: userData }
})

// ✅ gen хорош для сложной бизнес-логики
const businessLogic = Effect.gen(function* () {
  const order = yield* getOrder(orderId)
  
  // Проверяем статус
  if (order.status !== "pending") {
    yield* Effect.fail(new OrderNotPendingError(orderId))
  }
  
  // Получаем связанные данные
  const customer = yield* getCustomer(order.customerId)
  const inventory = yield* checkInventory(order.items)
  
  // Бизнес-правила
  if (inventory.hasOutOfStock) {
    yield* notifyCustomer(customer.email, "outOfStock")
    yield* Effect.fail(new OutOfStockError(inventory.outOfStockItems))
  }
  
  // Обработка
  const payment = yield* processPayment(order, customer)
  const shipment = yield* createShipment(order, customer)
  
  return {
    order,
    payment,
    shipment,
    estimatedDelivery: shipment.estimatedDate
  }
})

Комбинирование подходов


// Можно комбинировать gen и pipe
const combined = Effect.gen(function* () {
  // Используем pipe для операторов
  const user = yield* pipe(
    fetchUser(id),
    Effect.timeout(Duration.seconds(5)),
    Effect.retry({ times: 3 }),
    Effect.catchTag("TimeoutException", () =>
      Effect.succeed({ id, name: "Unknown", cached: true })
    )
  )
  
  // Обычная логика в генераторе
  if (user.cached) {
    yield* Effect.log("Using cached user data")
    return { user, fresh: false }
  }
  
  // Ещё один pipe
  const orders = yield* pipe(
    getOrders(user.id),
    Effect.map((orders) => orders.filter((o) => o.status === "active"))
  )
  
  return { user, orders, fresh: true }
})

// Или gen внутри pipe
const genInsidePipe = pipe(
  getConfig(),
  Effect.flatMap((config) =>
    Effect.gen(function* () {
      const db = yield* connectToDb(config.dbUrl)
      const cache = yield* connectToCache(config.cacheUrl)
      return { db, cache }
    })
  ),
  Effect.tap(({ db, cache }) =>
    Effect.log(`Connected to ${db.name} and ${cache.name}`)
  )
)

Do-notation

Effect.Do — альтернативный синтаксис

Effect.Do предоставляет способ накопления значений без генераторов:


// Do-notation style
const withDo = pipe(
  Effect.Do,
  Effect.bind("user", () => getUser(id)),
  Effect.bind("orders", ({ user }) => getOrders(user.id)),
  Effect.bind("total", ({ orders }) => calculateTotal(orders)),
  Effect.let("count", ({ orders }) => orders.length),
  Effect.map(({ user, orders, total, count }) => ({
    user,
    orders,
    total,
    averageValue: count > 0 ? total / count : 0
  }))
)

// Эквивалент с генератором
const withGen = Effect.gen(function* () {
  const user = yield* getUser(id)
  const orders = yield* getOrders(user.id)
  const total = yield* calculateTotal(orders)
  const count = orders.length
  
  return {
    user,
    orders,
    total,
    averageValue: count > 0 ? total / count : 0
  }
})

API Do-notation


// ─────────────────────────────────────────────────────────────────
// Effect.Do — начало цепочки, возвращает Effect<{}>
// ─────────────────────────────────────────────────────────────────

const start = Effect.Do
//    ^? Effect<{}, never, never>


// ─────────────────────────────────────────────────────────────────
// Effect.bind — добавить эффект, результат добавляется в объект
// ─────────────────────────────────────────────────────────────────

const withBind = pipe(
  Effect.Do,
  Effect.bind("a", () => Effect.succeed(1)),
  Effect.bind("b", ({ a }) => Effect.succeed(a + 1)),
  Effect.bind("c", ({ a, b }) => Effect.succeed(a + b))
)
//    ^? Effect<{ a: number; b: number; c: number }, never, never>


// ─────────────────────────────────────────────────────────────────
// Effect.let — добавить чистое значение (не Effect)
// ─────────────────────────────────────────────────────────────────

const withLet = pipe(
  Effect.Do,
  Effect.bind("items", () => Effect.succeed([1, 2, 3])),
  Effect.let("count", ({ items }) => items.length),
  Effect.let("sum", ({ items }) => items.reduce((a, b) => a + b, 0))
)
//    ^? Effect<{ items: number[]; count: number; sum: number }, never, never>


// ─────────────────────────────────────────────────────────────────
// Effect.bindTo — преобразовать Effect<A> в Effect<{ name: A }>
// ─────────────────────────────────────────────────────────────────

const withBindTo = pipe(
  Effect.succeed(42),
  Effect.bindTo("answer")
)
//    ^? Effect<{ answer: number }, never, never>


// ─────────────────────────────────────────────────────────────────
// Комбинирование
// ─────────────────────────────────────────────────────────────────

const complex = pipe(
  getUser(id),
  Effect.bindTo("user"),
  Effect.bind("orders", ({ user }) => getOrders(user.id)),
  Effect.let("hasOrders", ({ orders }) => orders.length > 0),
  Effect.bind("recommendations", ({ user, hasOrders }) =>
    hasOrders
      ? getRecommendations(user.id)
      : Effect.succeed([])
  ),
  Effect.map(({ user, orders, recommendations }) => ({
    user,
    orderCount: orders.length,
    recommendations
  }))
)

Когда использовать Do-notation

// ✅ Do хорош когда нужно накопить несколько значений
// и каждое следующее зависит от предыдущих

// ❌ Избегайте Do для простых линейных цепочек
const tooSimple = pipe(
  Effect.Do,
  Effect.bind("x", () => Effect.succeed(1)),
  Effect.map(({ x }) => x * 2)
) // Лучше просто pipe с map

// ✅ Do хорош для "accumulator" паттерна
const accumulator = pipe(
  Effect.Do,
  Effect.bind("config", () => loadConfig()),
  Effect.bind("db", ({ config }) => connectDb(config.dbUrl)),
  Effect.bind("cache", ({ config }) => connectCache(config.cacheUrl)),
  Effect.bind("metrics", ({ config }) => setupMetrics(config.metricsUrl)),
  Effect.map(({ db, cache, metrics }) => ({
    db,
    cache,
    metrics,
    // Все три нужны для создания сервиса
  }))
)

Продвинутые техники

Вложенные генераторы


// Генераторы можно вкладывать друг в друга
const outer = Effect.gen(function* () {
  yield* Effect.log("Outer start")
  
  const innerResult = yield* Effect.gen(function* () {
    yield* Effect.log("Inner start")
    const a = yield* Effect.succeed(1)
    const b = yield* Effect.succeed(2)
    yield* Effect.log("Inner end")
    return a + b
  })
  
  yield* Effect.log(`Inner result: ${innerResult}`)
  yield* Effect.log("Outer end")
  
  return innerResult * 2
})

Генераторы с сервисами


// Определение сервиса
class Logger extends Context.Tag("Logger")<
  Logger,
  {
    readonly info: (msg: string) => Effect.Effect<void>
    readonly error: (msg: string, err?: unknown) => Effect.Effect<void>
  }
>() {}

class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User | null>
    readonly save: (user: User) => Effect.Effect<User>
  }
>() {}

interface User {
  readonly id: string
  readonly name: string
  readonly email: string
}

// Использование сервисов в генераторе
const updateUserEmail = (
  userId: string,
  newEmail: string
): Effect.Effect<User, Error, Logger | UserRepository> =>
  Effect.gen(function* () {
    // Получаем сервисы
    const logger = yield* Logger
    const repo = yield* UserRepository
    
    yield* logger.info(`Updating email for user ${userId}`)
    
    // Используем сервисы
    const user = yield* repo.findById(userId)
    
    if (user === null) {
      yield* logger.error(`User ${userId} not found`)
      yield* Effect.fail(new Error(`User ${userId} not found`))
    }
    
    const updatedUser = { ...user!, email: newEmail }
    const saved = yield* repo.save(updatedUser)
    
    yield* logger.info(`Email updated for user ${userId}`)
    
    return saved
  })

// Реализации сервисов
const LoggerLive = Layer.succeed(Logger, {
  info: (msg) => Effect.sync(() => console.log(`[INFO] ${msg}`)),
  error: (msg, err) => Effect.sync(() => console.error(`[ERROR] ${msg}`, err))
})

const UserRepositoryLive = Layer.succeed(UserRepository, {
  findById: (id) => Effect.succeed({ id, name: "Alice", email: "old@example.com" }),
  save: (user) => Effect.succeed(user)
})

// Запуск
const program = pipe(
  updateUserEmail("user-123", "new@example.com"),
  Effect.provide(Layer.merge(LoggerLive, UserRepositoryLive))
)

Параллельное выполнение внутри генератора


interface User { readonly id: string; readonly name: string }
interface Order { readonly id: string; readonly total: number }
interface Recommendation { readonly id: string; readonly title: string }

const fetchUser = (id: string): Effect.Effect<User, Error> =>
  pipe(
    Effect.succeed({ id, name: "Alice" }),
    Effect.delay(Duration.millis(100))
  )

const fetchOrders = (userId: string): Effect.Effect<Order[], Error> =>
  pipe(
    Effect.succeed([{ id: "o1", total: 100 }]),
    Effect.delay(Duration.millis(150))
  )

const fetchRecommendations = (userId: string): Effect.Effect<Recommendation[], Error> =>
  pipe(
    Effect.succeed([{ id: "r1", title: "Product A" }]),
    Effect.delay(Duration.millis(200))
  )

// Параллельное выполнение независимых операций
const getUserDashboard = (userId: string) =>
  Effect.gen(function* () {
    // Сначала получаем пользователя (нужен для дальнейших запросов)
    const user = yield* fetchUser(userId)
    
    // Затем параллельно загружаем orders и recommendations
    const [orders, recommendations] = yield* Effect.all(
      [
        fetchOrders(user.id),
        fetchRecommendations(user.id)
      ],
      { concurrency: "unbounded" }
    )
    
    return {
      user,
      orders,
      recommendations,
      totalSpent: orders.reduce((sum, o) => sum + o.total, 0)
    }
  })

// Ещё более параллельный вариант
const getUserDashboardParallel = (userId: string) =>
  Effect.gen(function* () {
    // Если user не нужен для других запросов, можно всё параллельно
    const { user, orders, recommendations } = yield* Effect.all(
      {
        user: fetchUser(userId),
        orders: fetchOrders(userId),
        recommendations: fetchRecommendations(userId)
      },
      { concurrency: "unbounded" }
    )
    
    return {
      user,
      orders,
      recommendations,
      totalSpent: orders.reduce((sum, o) => sum + o.total, 0)
    }
  })

Обработка Option и Either в генераторах


// Effect.gen работает с Option через специальные операторы
const withOption = Effect.gen(function* () {
  const maybeValue: Option.Option<number> = Option.some(42)
  
  // Вариант 1: Преобразовать Option в Effect
  const value = yield* Effect.fromOption(maybeValue, () => "Value is None")
  
  // Вариант 2: Использовать Option.getOrElse
  const valueOrDefault = Option.getOrElse(maybeValue, () => 0)
  
  return { value, valueOrDefault }
})

// Effect.gen работает с Either
const withEither = Effect.gen(function* () {
  const result: Either.Either<string, number> = Either.right(42)
  
  // Преобразовать Either в Effect
  const value = yield* Effect.fromEither(result)
  
  return value
})

// Практический пример: поиск с fallback
const findUserOrCreate = (
  email: string
): Effect.Effect<User, Error> =>
  Effect.gen(function* () {
    // findByEmail возвращает Option
    const maybeUser = yield* findUserByEmail(email)
    
    // Если нашли — возвращаем
    if (Option.isSome(maybeUser)) {
      return maybeUser.value
    }
    
    // Иначе создаём нового
    yield* Effect.log(`User with email ${email} not found, creating...`)
    const newUser = yield* createUser({ email, name: email.split("@")[0] })
    
    return newUser
  })

interface User {
  readonly id: string
  readonly name: string
  readonly email: string
}

const findUserByEmail = (email: string): Effect.Effect<Option.Option<User>> =>
  Effect.succeed(Option.none())

const createUser = (data: { email: string; name: string }): Effect.Effect<User, Error> =>
  Effect.succeed({ id: "new-user", ...data })

Примеры

Пример 1: HTTP запрос с полной обработкой


// ═══════════════════════════════════════════════════════════════════
// Типы ошибок
// ═══════════════════════════════════════════════════════════════════

class NetworkError extends Data.TaggedError("NetworkError")<{
  readonly url: string
  readonly cause: unknown
}> {}

class HttpError extends Data.TaggedError("HttpError")<{
  readonly status: number
  readonly statusText: string
  readonly url: string
}> {}

class ParseError extends Data.TaggedError("ParseError")<{
  readonly body: string
  readonly error: unknown
}> {}

class TimeoutError extends Data.TaggedError("TimeoutError")<{
  readonly url: string
  readonly timeout: Duration.Duration
}> {}

type FetchError = NetworkError | HttpError | ParseError | TimeoutError

// ═══════════════════════════════════════════════════════════════════
// HTTP клиент
// ═══════════════════════════════════════════════════════════════════

interface FetchOptions {
  readonly method?: "GET" | "POST" | "PUT" | "DELETE"
  readonly headers?: Record<string, string>
  readonly body?: unknown
  readonly timeout?: Duration.Duration
}

const fetchJson = <T>(
  url: string,
  options: FetchOptions = {}
): Effect.Effect<T, FetchError> =>
  Effect.gen(function* () {
    const timeout = options.timeout ?? Duration.seconds(30)
    
    yield* Effect.log(`Fetching ${url}...`)
    
    // 1. Выполняем запрос
    const response = yield* pipe(
      Effect.tryPromise({
        try: () => fetch(url, {
          method: options.method ?? "GET",
          headers: {
            "Content-Type": "application/json",
            ...options.headers
          },
          body: options.body ? JSON.stringify(options.body) : undefined
        }),
        catch: (cause) => new NetworkError({ url, cause })
      }),
      Effect.timeout(timeout),
      Effect.catchTag("TimeoutException", () =>
        Effect.fail(new TimeoutError({ url, timeout }))
      )
    )
    
    // 2. Проверяем HTTP статус
    if (!response.ok) {
      yield* Effect.log(`HTTP error: ${response.status} ${response.statusText}`)
      yield* Effect.fail(new HttpError({
        status: response.status,
        statusText: response.statusText,
        url
      }))
    }
    
    // 3. Читаем тело
    const text = yield* Effect.tryPromise({
      try: () => response.text(),
      catch: (cause) => new NetworkError({ url, cause })
    })
    
    // 4. Парсим JSON
    const data = yield* Effect.try({
      try: () => JSON.parse(text) as T,
      catch: (error) => new ParseError({ body: text.slice(0, 200), error })
    })
    
    yield* Effect.log(`Successfully fetched ${url}`)
    
    return data
  })

// ═══════════════════════════════════════════════════════════════════
// Использование
// ═══════════════════════════════════════════════════════════════════

interface Post {
  readonly id: number
  readonly title: string
  readonly body: string
  readonly userId: number
}

interface User {
  readonly id: number
  readonly name: string
  readonly email: string
}

const getPostWithAuthor = (postId: number) =>
  Effect.gen(function* () {
    // Получаем пост
    const post = yield* pipe(
      fetchJson<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`),
      Effect.retry(
        Schedule.exponential(Duration.millis(100)).pipe(
          Schedule.compose(Schedule.recurs(3))
        )
      )
    )
    
    yield* Effect.log(`Got post: ${post.title}`)
    
    // Получаем автора
    const author = yield* fetchJson<User>(
      `https://jsonplaceholder.typicode.com/users/${post.userId}`
    )
    
    yield* Effect.log(`Got author: ${author.name}`)
    
    return {
      post,
      author,
      summary: `"${post.title}" by ${author.name}`
    }
  })

// С обработкой ошибок
const program = pipe(
  getPostWithAuthor(1),
  Effect.catchTags({
    NetworkError: (e) =>
      Effect.succeed({
        post: null,
        author: null,
        summary: `Network error for ${e.url}`
      }),
    HttpError: (e) =>
      Effect.succeed({
        post: null,
        author: null,
        summary: `HTTP ${e.status} for ${e.url}`
      }),
    ParseError: (e) =>
      Effect.succeed({
        post: null,
        author: null,
        summary: `Parse error: ${e.body.slice(0, 50)}...`
      }),
    TimeoutError: (e) =>
      Effect.succeed({
        post: null,
        author: null,
        summary: `Timeout after ${Duration.toSeconds(e.timeout)}s for ${e.url}`
      })
  }),
  Effect.tap((result) => Effect.log(`Result: ${result.summary}`))
)

Пример 2: Пакетная обработка с прогрессом


// ═══════════════════════════════════════════════════════════════════
// Типы
// ═══════════════════════════════════════════════════════════════════

interface ProcessingResult<T> {
  readonly successful: ReadonlyArray<T>
  readonly failed: ReadonlyArray<{ readonly item: T; readonly error: string }>
  readonly totalProcessed: number
  readonly totalFailed: number
  readonly duration: Duration.Duration
}

interface ProgressInfo {
  readonly current: number
  readonly total: number
  readonly percentage: number
  readonly estimatedRemaining: Duration.Duration | null
}

// ═══════════════════════════════════════════════════════════════════
// Batch processor
// ═══════════════════════════════════════════════════════════════════

const processBatch = <T, R>(
  items: ReadonlyArray<T>,
  process: (item: T) => Effect.Effect<R, Error>,
  options: {
    readonly batchSize: number
    readonly onProgress?: (progress: ProgressInfo) => Effect.Effect<void>
    readonly concurrency?: number
  }
): Effect.Effect<ProcessingResult<R>> =>
  Effect.gen(function* () {
    const startTime = yield* Effect.sync(() => Date.now())
    const concurrency = options.concurrency ?? 5
    
    // Состояние прогресса
    const processedRef = yield* Ref.make(0)
    const successfulRef = yield* Ref.make<R[]>([])
    const failedRef = yield* Ref.make<Array<{ item: T; error: string }>>([])
    
    // Разбиваем на батчи
    const batches = Chunk.toReadonlyArray(
      Chunk.chunksOf(Chunk.fromIterable(items), options.batchSize)
    )
    
    yield* Effect.log(`Processing ${items.length} items in ${batches.length} batches`)
    
    // Обрабатываем батчи последовательно
    for (const batch of batches) {
      const batchItems = Chunk.toReadonlyArray(batch)
      
      // Внутри батча — параллельно
      yield* Effect.forEach(
        batchItems,
        (item) =>
          Effect.gen(function* () {
            const result = yield* pipe(
              process(item),
              Effect.either
            )
            
            // Обновляем состояние
            yield* Ref.update(processedRef, (n) => n + 1)
            
            if (result._tag === "Right") {
              yield* Ref.update(successfulRef, (arr) => [...arr, result.right])
            } else {
              yield* Ref.update(failedRef, (arr) => [
                ...arr,
                { item, error: result.left.message }
              ])
            }
            
            // Отправляем прогресс
            if (options.onProgress) {
              const current = yield* Ref.get(processedRef)
              const elapsed = Date.now() - startTime
              const rate = current / elapsed
              const remaining = items.length - current
              
              yield* options.onProgress({
                current,
                total: items.length,
                percentage: Math.round((current / items.length) * 100),
                estimatedRemaining:
                  rate > 0
                    ? Duration.millis(remaining / rate)
                    : null
              })
            }
          }),
        { concurrency }
      )
    }
    
    // Собираем результат
    const endTime = yield* Effect.sync(() => Date.now())
    const successful = yield* Ref.get(successfulRef)
    const failed = yield* Ref.get(failedRef)
    
    return {
      successful,
      failed,
      totalProcessed: successful.length,
      totalFailed: failed.length,
      duration: Duration.millis(endTime - startTime)
    }
  })

// ═══════════════════════════════════════════════════════════════════
// Использование
// ═══════════════════════════════════════════════════════════════════

interface Order {
  readonly id: string
  readonly amount: number
}

interface ProcessedOrder {
  readonly orderId: string
  readonly status: "processed"
  readonly processedAt: Date
}

const processOrder = (order: Order): Effect.Effect<ProcessedOrder, Error> =>
  Effect.gen(function* () {
    // Симуляция обработки
    yield* Effect.sleep(Duration.millis(Math.random() * 100))
    
    // Случайная ошибка для демонстрации
    if (Math.random() < 0.1) {
      yield* Effect.fail(new Error(`Failed to process order ${order.id}`))
    }
    
    return {
      orderId: order.id,
      status: "processed" as const,
      processedAt: new Date()
    }
  })

const orders: ReadonlyArray<Order> = Array.from(
  { length: 100 },
  (_, i) => ({ id: `order-${i + 1}`, amount: Math.random() * 1000 })
)

const program = Effect.gen(function* () {
  yield* Effect.log("Starting batch processing...")
  
  const result = yield* processBatch(orders, processOrder, {
    batchSize: 10,
    concurrency: 5,
    onProgress: (progress) =>
      Effect.log(
        `Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`
      )
  })
  
  yield* Effect.log(`
=== Processing Complete ===
Successful: ${result.totalProcessed}
Failed: ${result.totalFailed}
Duration: ${Duration.toSeconds(result.duration).toFixed(2)}s
  `)
  
  if (result.failed.length > 0) {
    yield* Effect.log("Failed items:")
    for (const failure of result.failed.slice(0, 5)) {
      yield* Effect.log(`  - ${failure.item.id}: ${failure.error}`)
    }
    if (result.failed.length > 5) {
      yield* Effect.log(`  ... and ${result.failed.length - 5} more`)
    }
  }
  
  return result
})

Пример 3: Транзакционная бизнес-логика


// ═══════════════════════════════════════════════════════════════════
// Domain types
// ═══════════════════════════════════════════════════════════════════

interface Account {
  readonly id: string
  readonly balance: number
  readonly currency: string
}

interface Transfer {
  readonly id: string
  readonly fromAccountId: string
  readonly toAccountId: string
  readonly amount: number
  readonly currency: string
  readonly status: "pending" | "completed" | "failed"
  readonly timestamp: Date
}

// ═══════════════════════════════════════════════════════════════════
// Errors
// ═══════════════════════════════════════════════════════════════════

class InsufficientFundsError extends Data.TaggedError("InsufficientFundsError")<{
  readonly accountId: string
  readonly required: number
  readonly available: number
}> {}

class AccountNotFoundError extends Data.TaggedError("AccountNotFoundError")<{
  readonly accountId: string
}> {}

class CurrencyMismatchError extends Data.TaggedError("CurrencyMismatchError")<{
  readonly expected: string
  readonly actual: string
}> {}

type TransferError =
  | InsufficientFundsError
  | AccountNotFoundError
  | CurrencyMismatchError

// ═══════════════════════════════════════════════════════════════════
// Services
// ═══════════════════════════════════════════════════════════════════

class AccountRepository extends Context.Tag("AccountRepository")<
  AccountRepository,
  {
    readonly findById: (id: string) => Effect.Effect<Account | null>
    readonly updateBalance: (id: string, newBalance: number) => Effect.Effect<Account>
  }
>() {}

class TransferRepository extends Context.Tag("TransferRepository")<
  TransferRepository,
  {
    readonly create: (transfer: Omit<Transfer, "id">) => Effect.Effect<Transfer>
    readonly updateStatus: (
      id: string,
      status: Transfer["status"]
    ) => Effect.Effect<Transfer>
  }
>() {}

class EventPublisher extends Context.Tag("EventPublisher")<
  EventPublisher,
  {
    readonly publish: (event: { type: string; payload: unknown }) => Effect.Effect<void>
  }
>() {}

// ═══════════════════════════════════════════════════════════════════
// Business logic
// ═══════════════════════════════════════════════════════════════════

const transferMoney = (
  fromAccountId: string,
  toAccountId: string,
  amount: number,
  currency: string
): Effect.Effect<
  Transfer,
  TransferError,
  AccountRepository | TransferRepository | EventPublisher
> =>
  Effect.gen(function* () {
    const accountRepo = yield* AccountRepository
    const transferRepo = yield* TransferRepository
    const eventPublisher = yield* EventPublisher

    yield* Effect.log(`Starting transfer: ${amount} ${currency} from ${fromAccountId} to ${toAccountId}`)

    // 1. Валидация: получаем счета
    const fromAccount = yield* Effect.gen(function* () {
      const account = yield* accountRepo.findById(fromAccountId)
      if (account === null) {
        yield* Effect.fail(new AccountNotFoundError({ accountId: fromAccountId }))
      }
      return account!
    })

    const toAccount = yield* Effect.gen(function* () {
      const account = yield* accountRepo.findById(toAccountId)
      if (account === null) {
        yield* Effect.fail(new AccountNotFoundError({ accountId: toAccountId }))
      }
      return account!
    })

    // 2. Проверка валюты
    if (fromAccount.currency !== currency) {
      yield* Effect.fail(new CurrencyMismatchError({
        expected: currency,
        actual: fromAccount.currency
      }))
    }

    if (toAccount.currency !== currency) {
      yield* Effect.fail(new CurrencyMismatchError({
        expected: currency,
        actual: toAccount.currency
      }))
    }

    // 3. Проверка баланса
    if (fromAccount.balance < amount) {
      yield* Effect.fail(new InsufficientFundsError({
        accountId: fromAccountId,
        required: amount,
        available: fromAccount.balance
      }))
    }

    // 4. Создаём запись о трансфере
    const transfer = yield* transferRepo.create({
      fromAccountId,
      toAccountId,
      amount,
      currency,
      status: "pending",
      timestamp: new Date()
    })

    yield* Effect.log(`Transfer ${transfer.id} created`)

    // 5. Выполняем транзакцию
    yield* pipe(
      Effect.gen(function* () {
        // Списываем со счёта отправителя
        yield* accountRepo.updateBalance(
          fromAccountId,
          fromAccount.balance - amount
        )

        // Зачисляем на счёт получателя
        yield* accountRepo.updateBalance(
          toAccountId,
          toAccount.balance + amount
        )

        // Обновляем статус трансфера
        yield* transferRepo.updateStatus(transfer.id, "completed")
      }),
      // В случае ошибки — откатываем
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`Transfer ${transfer.id} failed, rolling back...`)
          yield* transferRepo.updateStatus(transfer.id, "failed")
          yield* Effect.fail(error)
        })
      )
    )

    // 6. Публикуем событие
    yield* eventPublisher.publish({
      type: "TransferCompleted",
      payload: {
        transferId: transfer.id,
        fromAccountId,
        toAccountId,
        amount,
        currency
      }
    })

    yield* Effect.log(`Transfer ${transfer.id} completed successfully`)

    return { ...transfer, status: "completed" as const }
  })

// ═══════════════════════════════════════════════════════════════════
// In-memory implementations for testing
// ═══════════════════════════════════════════════════════════════════

const makeTestAccountRepository = Effect.gen(function* () {
  const accounts = yield* Ref.make<Map<string, Account>>(
    new Map([
      ["acc-1", { id: "acc-1", balance: 1000, currency: "USD" }],
      ["acc-2", { id: "acc-2", balance: 500, currency: "USD" }]
    ])
  )

  return AccountRepository.of({
    findById: (id) =>
      Effect.gen(function* () {
        const map = yield* Ref.get(accounts)
        return map.get(id) ?? null
      }),
    updateBalance: (id, newBalance) =>
      Effect.gen(function* () {
        const account = { id, balance: newBalance, currency: "USD" }
        yield* Ref.update(accounts, (map) => new Map(map).set(id, account))
        return account
      })
  })
})

const makeTestTransferRepository = Effect.gen(function* () {
  const transfers = yield* Ref.make<Map<string, Transfer>>(new Map())
  const idCounter = yield* Ref.make(0)

  return TransferRepository.of({
    create: (data) =>
      Effect.gen(function* () {
        const id = yield* Ref.updateAndGet(idCounter, (n) => n + 1)
        const transfer: Transfer = { ...data, id: `transfer-${id}` }
        yield* Ref.update(transfers, (map) => new Map(map).set(transfer.id, transfer))
        return transfer
      }),
    updateStatus: (id, status) =>
      Effect.gen(function* () {
        const map = yield* Ref.get(transfers)
        const transfer = map.get(id)!
        const updated = { ...transfer, status }
        yield* Ref.update(transfers, (m) => new Map(m).set(id, updated))
        return updated
      })
  })
})

const TestEventPublisher = Layer.succeed(EventPublisher, {
  publish: (event) => Effect.log(`Event: ${event.type}`)
})

// ═══════════════════════════════════════════════════════════════════
// Run
// ═══════════════════════════════════════════════════════════════════

const TestAccountRepositoryLayer = Layer.effect(
  AccountRepository,
  makeTestAccountRepository
)

const TestTransferRepositoryLayer = Layer.effect(
  TransferRepository,
  makeTestTransferRepository
)

const TestLayer = Layer.mergeAll(
  TestAccountRepositoryLayer,
  TestTransferRepositoryLayer,
  TestEventPublisher
)

const program = pipe(
  transferMoney("acc-1", "acc-2", 200, "USD"),
  Effect.tap((transfer) =>
    Effect.log(`Transfer result: ${JSON.stringify(transfer)}`)
  ),
  Effect.catchTags({
    AccountNotFoundError: (e) =>
      Effect.log(`Account not found: ${e.accountId}`),
    InsufficientFundsError: (e) =>
      Effect.log(`Insufficient funds: need ${e.required}, have ${e.available}`),
    CurrencyMismatchError: (e) =>
      Effect.log(`Currency mismatch: expected ${e.expected}, got ${e.actual}`)
  }),
  Effect.provide(TestLayer)
)

Effect.runPromise(program)

Упражнения

Базовый: Конвертация pipe в gen

Перепишите следующий код с pipe на Effect.gen:


// Исходный код с pipe
const fetchUserData = (userId: string) =>
  pipe(
    getUser(userId),
    Effect.flatMap((user) =>
      pipe(
        getOrders(user.id),
        Effect.flatMap((orders) =>
          pipe(
            calculateTotal(orders),
            Effect.map((total) => ({
              user,
              orders,
              total,
              averageOrderValue: orders.length > 0 ? total / orders.length : 0
            }))
          )
        )
      )
    )
  )

// TODO: Перепишите на Effect.gen
const fetchUserDataGen = (userId: string) =>
  ???

Решение


interface User {
  readonly id: string
  readonly name: string
}

interface Order {
  readonly id: string
  readonly amount: number
}

const getUser = (id: string): Effect.Effect<User, Error> =>
  Effect.succeed({ id, name: "Alice" })

const getOrders = (userId: string): Effect.Effect<Order[], Error> =>
  Effect.succeed([
    { id: "o1", amount: 100 },
    { id: "o2", amount: 200 }
  ])

const calculateTotal = (orders: Order[]): Effect.Effect<number, Error> =>
  Effect.succeed(orders.reduce((sum, o) => sum + o.amount, 0))

// Решение с Effect.gen
const fetchUserDataGen = (userId: string) =>
  Effect.gen(function* () {
    const user = yield* getUser(userId)
    const orders = yield* getOrders(user.id)
    const total = yield* calculateTotal(orders)
    
    return {
      user,
      orders,
      total,
      averageOrderValue: orders.length > 0 ? total / orders.length : 0
    }
  })

// Тест
Effect.runPromise(fetchUserDataGen("user-1")).then(console.log)
// { user: { id: 'user-1', name: 'Alice' }, orders: [...], total: 300, averageOrderValue: 150 }

Продвинутый: Поиск с фолбеками

Реализуйте функцию поиска данных, которая последовательно пробует разные источники:


interface Product {
  readonly id: string
  readonly name: string
  readonly price: number
}

// Источники данных (от самого быстрого к самому медленному)
declare const getFromCache: (id: string) => Effect.Effect<Option.Option<Product>>
declare const getFromLocalDb: (id: string) => Effect.Effect<Option.Option<Product>, Error>
declare const getFromRemoteApi: (id: string) => Effect.Effect<Product, Error>

// TODO: Реализуйте функцию, которая:
// 1. Сначала проверяет кеш
// 2. Если в кеше нет — проверяет локальную БД
// 3. Если в локальной БД нет — запрашивает API
// 4. После успешного получения из API — сохраняет в кеш
// 5. Возвращает Product или ошибку NotFoundError

class NotFoundError {
  readonly _tag = "NotFoundError"
  constructor(readonly productId: string) {}
}

const findProduct = (
  id: string
): Effect.Effect<Product, NotFoundError | Error> =>
  ???

Решение


interface Product {
  readonly id: string
  readonly name: string
  readonly price: number
}

class NotFoundError {
  readonly _tag = "NotFoundError"
  constructor(readonly productId: string) {}
}

// Симуляция источников данных
const cache = new Map<string, Product>()

const getFromCache = (id: string): Effect.Effect<Option.Option<Product>> =>
  Effect.gen(function* () {
    yield* Effect.log(`[Cache] Looking for ${id}`)
    yield* Effect.sleep(Duration.millis(10))
    const product = cache.get(id)
    if (product) {
      yield* Effect.log(`[Cache] Found ${id}`)
      return Option.some(product)
    }
    yield* Effect.log(`[Cache] Miss for ${id}`)
    return Option.none()
  })

const saveToCache = (product: Product): Effect.Effect<void> =>
  Effect.gen(function* () {
    yield* Effect.log(`[Cache] Saving ${product.id}`)
    cache.set(product.id, product)
  })

const getFromLocalDb = (id: string): Effect.Effect<Option.Option<Product>, Error> =>
  Effect.gen(function* () {
    yield* Effect.log(`[LocalDB] Looking for ${id}`)
    yield* Effect.sleep(Duration.millis(50))
    // Симуляция: только "local-1" есть в локальной БД
    if (id === "local-1") {
      yield* Effect.log(`[LocalDB] Found ${id}`)
      return Option.some({ id, name: "Local Product", price: 99 })
    }
    yield* Effect.log(`[LocalDB] Not found ${id}`)
    return Option.none()
  })

const getFromRemoteApi = (id: string): Effect.Effect<Product, Error> =>
  Effect.gen(function* () {
    yield* Effect.log(`[API] Fetching ${id}`)
    yield* Effect.sleep(Duration.millis(200))
    // Симуляция: "api-1" есть в API, остальные — нет
    if (id === "api-1") {
      yield* Effect.log(`[API] Found ${id}`)
      return { id, name: "Remote Product", price: 199 }
    }
    yield* Effect.log(`[API] Not found ${id}`)
    yield* Effect.fail(new Error(`Product ${id} not found in API`))
  })

// Решение
const findProduct = (
  id: string
): Effect.Effect<Product, NotFoundError | Error> =>
  Effect.gen(function* () {
    yield* Effect.log(`=== Finding product ${id} ===`)
    
    // 1. Проверяем кеш
    const cached = yield* getFromCache(id)
    if (Option.isSome(cached)) {
      yield* Effect.log(`Found in cache!`)
      return cached.value
    }
    
    // 2. Проверяем локальную БД
    const local = yield* getFromLocalDb(id)
    if (Option.isSome(local)) {
      yield* Effect.log(`Found in local DB!`)
      // Сохраняем в кеш для следующего раза
      yield* saveToCache(local.value)
      return local.value
    }
    
    // 3. Запрашиваем API
    const remote = yield* pipe(
      getFromRemoteApi(id),
      Effect.catchAll((error) =>
        Effect.fail(new NotFoundError(id))
      )
    )
    
    yield* Effect.log(`Found in API!`)
    
    // 4. Сохраняем в кеш
    yield* saveToCache(remote)
    
    return remote
  })

// Тесты
const test = Effect.gen(function* () {
  // Тест 1: Продукт есть в API
  const result1 = yield* pipe(
    findProduct("api-1"),
    Effect.either
  )
  console.log("\nResult 1:", result1)
  
  // Тест 2: Тот же продукт — теперь в кеше
  const result2 = yield* pipe(
    findProduct("api-1"),
    Effect.either
  )
  console.log("\nResult 2:", result2)
  
  // Тест 3: Продукт в локальной БД
  const result3 = yield* pipe(
    findProduct("local-1"),
    Effect.either
  )
  console.log("\nResult 3:", result3)
  
  // Тест 4: Продукта нет нигде
  const result4 = yield* pipe(
    findProduct("nonexistent"),
    Effect.either
  )
  console.log("\nResult 4:", result4)
})

Effect.runPromise(test)

Экспертный: Saga Pattern с компенсациями

Реализуйте паттерн Saga для распределённой транзакции с автоматическими компенсациями:


// Saga step: action + compensation
interface SagaStep<A, E> {
  readonly name: string
  readonly action: Effect.Effect<A, E>
  readonly compensation: (result: A) => Effect.Effect<void, never>
}

// TODO: Реализуйте Saga executor
// Требования:
// 1. Выполняет шаги последовательно
// 2. Если шаг падает — откатывает все предыдущие шаги в обратном порядке
// 3. Логирует каждый шаг и откат
// 4. Возвращает результаты всех шагов или ошибку с информацией о точке сбоя

class SagaError extends Data.TaggedError("SagaError")<{
  readonly failedStep: string
  readonly originalError: unknown
  readonly compensatedSteps: ReadonlyArray<string>
}> {}

const executeSaga = <Steps extends readonly SagaStep<any, any>[]>(
  name: string,
  steps: Steps
): Effect.Effect<
  { [K in keyof Steps]: Steps[K] extends SagaStep<infer A, any> ? A : never },
  SagaError
> =>
  ???

// Пример использования
const orderSaga = executeSaga("CreateOrder", [
  {
    name: "ReserveInventory",
    action: reserveInventory(orderId, items),
    compensation: (reservation) => releaseInventory(reservation.id)
  },
  {
    name: "ChargePayment",
    action: chargePayment(orderId, amount),
    compensation: (payment) => refundPayment(payment.transactionId)
  },
  {
    name: "CreateShipment",
    action: createShipment(orderId, address),
    compensation: (shipment) => cancelShipment(shipment.trackingId)
  }
] as const)

Решение


// Saga step definition
interface SagaStep<A, E> {
  readonly name: string
  readonly action: Effect.Effect<A, E>
  readonly compensation: (result: A) => Effect.Effect<void, never>
}

class SagaError extends Data.TaggedError("SagaError")<{
  readonly failedStep: string
  readonly originalError: unknown
  readonly compensatedSteps: ReadonlyArray<string>
}> {}

// Saga executor
const executeSaga = <Steps extends readonly SagaStep<any, any>[]>(
  name: string,
  steps: Steps
): Effect.Effect<
  { [K in keyof Steps]: Steps[K] extends SagaStep<infer A, any> ? A : never },
  SagaError
> =>
  Effect.gen(function* () {
    yield* Effect.log(`[Saga:${name}] Starting with ${steps.length} steps`)
    
    // Храним результаты для возможной компенсации
    const completedSteps = yield* Ref.make<Array<{
      name: string
      result: unknown
      compensation: (result: unknown) => Effect.Effect<void, never>
    }>>([])
    
    const results: unknown[] = []
    
    // Функция отката
    const rollback = (failedStep: string, error: unknown) =>
      Effect.gen(function* () {
        const completed = yield* Ref.get(completedSteps)
        const compensatedSteps: string[] = []
        
        yield* Effect.log(`[Saga:${name}] Rolling back ${completed.length} steps...`)
        
        // Откатываем в обратном порядке
        for (const step of [...completed].reverse()) {
          yield* Effect.log(`[Saga:${name}] Compensating: ${step.name}`)
          yield* step.compensation(step.result)
          compensatedSteps.push(step.name)
          yield* Effect.log(`[Saga:${name}] Compensated: ${step.name}`)
        }
        
        return new SagaError({
          failedStep,
          originalError: error,
          compensatedSteps
        })
      })
    
    // Выполняем шаги последовательно
    for (const step of steps) {
      yield* Effect.log(`[Saga:${name}] Executing: ${step.name}`)
      
      const result = yield* pipe(
        step.action,
        Effect.catchAll((error) =>
          Effect.gen(function* () {
            yield* Effect.log(`[Saga:${name}] Step failed: ${step.name}`)
            const sagaError = yield* rollback(step.name, error)
            yield* Effect.fail(sagaError)
          })
        )
      )
      
      // Сохраняем для возможной компенсации
      yield* Ref.update(completedSteps, (arr) => [
        ...arr,
        {
          name: step.name,
          result,
          compensation: step.compensation as (result: unknown) => Effect.Effect<void, never>
        }
      ])
      
      results.push(result)
      yield* Effect.log(`[Saga:${name}] Completed: ${step.name}`)
    }
    
    yield* Effect.log(`[Saga:${name}] All steps completed successfully`)
    
    return results as any
  })

// ═══════════════════════════════════════════════════════════════════
// Пример: Order Saga
// ═══════════════════════════════════════════════════════════════════

interface Reservation {
  readonly id: string
  readonly items: ReadonlyArray<{ productId: string; quantity: number }>
}

interface Payment {
  readonly transactionId: string
  readonly amount: number
}

interface Shipment {
  readonly trackingId: string
  readonly address: string
}

const reserveInventory = (
  orderId: string,
  items: ReadonlyArray<{ productId: string; quantity: number }>
): Effect.Effect<Reservation, Error> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(100))
    yield* Effect.log(`Reserving inventory for order ${orderId}`)
    return {
      id: `res-${orderId}`,
      items
    }
  })

const releaseInventory = (reservationId: string): Effect.Effect<void, never> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(50))
    yield* Effect.log(`Released inventory: ${reservationId}`)
  })

const chargePayment = (
  orderId: string,
  amount: number,
  shouldFail: boolean = false
): Effect.Effect<Payment, Error> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(150))
    
    if (shouldFail) {
      yield* Effect.fail(new Error("Payment declined"))
    }
    
    yield* Effect.log(`Charged payment for order ${orderId}`)
    return {
      transactionId: `pay-${orderId}`,
      amount
    }
  })

const refundPayment = (transactionId: string): Effect.Effect<void, never> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(50))
    yield* Effect.log(`Refunded payment: ${transactionId}`)
  })

const createShipment = (
  orderId: string,
  address: string
): Effect.Effect<Shipment, Error> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(100))
    yield* Effect.log(`Created shipment for order ${orderId}`)
    return {
      trackingId: `ship-${orderId}`,
      address
    }
  })

const cancelShipment = (trackingId: string): Effect.Effect<void, never> =>
  Effect.gen(function* () {
    yield* Effect.sleep(Duration.millis(50))
    yield* Effect.log(`Cancelled shipment: ${trackingId}`)
  })

// Тест: успешная saga
const testSuccess = Effect.gen(function* () {
  yield* Effect.log("\n=== TEST: Successful Saga ===\n")
  
  const result = yield* executeSaga("CreateOrder", [
    {
      name: "ReserveInventory",
      action: reserveInventory("order-1", [{ productId: "p1", quantity: 2 }]),
      compensation: (res) => releaseInventory(res.id)
    },
    {
      name: "ChargePayment",
      action: chargePayment("order-1", 100),
      compensation: (pay) => refundPayment(pay.transactionId)
    },
    {
      name: "CreateShipment",
      action: createShipment("order-1", "123 Main St"),
      compensation: (ship) => cancelShipment(ship.trackingId)
    }
  ] as const)
  
  yield* Effect.log(`\nSaga result: ${JSON.stringify(result, null, 2)}`)
})

// Тест: saga с откатом
const testRollback = Effect.gen(function* () {
  yield* Effect.log("\n=== TEST: Saga with Rollback ===\n")
  
  const result = yield* pipe(
    executeSaga("CreateOrder", [
      {
        name: "ReserveInventory",
        action: reserveInventory("order-2", [{ productId: "p1", quantity: 2 }]),
        compensation: (res) => releaseInventory(res.id)
      },
      {
        name: "ChargePayment",
        action: chargePayment("order-2", 100, true), // Эта операция упадёт
        compensation: (pay) => refundPayment(pay.transactionId)
      },
      {
        name: "CreateShipment",
        action: createShipment("order-2", "123 Main St"),
        compensation: (ship) => cancelShipment(ship.trackingId)
      }
    ] as const),
    Effect.either
  )
  
  if (result._tag === "Left") {
    yield* Effect.log(`\nSaga failed:`)
    yield* Effect.log(`  Failed step: ${result.left.failedStep}`)
    yield* Effect.log(`  Original error: ${result.left.originalError}`)
    yield* Effect.log(`  Compensated: ${result.left.compensatedSteps.join(", ")}`)
  }
})

// Запуск
const program = Effect.gen(function* () {
  yield* testSuccess
  yield* testRollback
})

Effect.runPromise(program)

Ключевые выводы

КонцепцияОписаниеКогда использовать
Effect.genГенератор для императивного стиляInline логика, простые computations
Effect.fnСоздание функции-генератораВнутренние функции, нужны stack traces
Effect.fn("name")Функция-генератор с трассировкойПубличные API, бизнес-логика, сервисы
Effect.fnUntracedФункция-генератор без трассировкиHot paths, утилиты, максимальная скорость
yield*Извлечение значения из EffectВнутри Effect.gen и Effect.fn
Do-notationАльтернатива с bind/letНакопление значений без генератора
pipeКомпозиция функцийЛинейные трансформации

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

  1. yield только с Effect*: Нельзя использовать yield* с обычными значениями
  2. Типы объединяются: Ошибки и зависимости автоматически объединяются в union
  3. Циклы: Используйте Effect.forEach или Effect.all вместо циклов с yield*
  4. Комбинируйте: Генераторы хорошо сочетаются с pipe внутри
  5. Вложенность: Генераторы можно вкладывать друг в друга
  6. Трассировка: Используйте Effect.fn("name") для сервисов и публичных API

Когда что использовать

СитуацияПодход
Простая линейная трансформацияpipe с map/flatMap
Inline логика с промежуточными значениямиEffect.gen
Условная логикаEffect.gen или Effect.fn
Сложная бизнес-логикаEffect.fn("name")
Методы сервисов с observabilityEffect.fn("ServiceName.method")
Hot path, утилитыEffect.fnUntraced
Накопление без генератораEffect.Do с bind/let