Effect Курс Конструкторы Effect

Конструкторы Effect

Effect предоставляет богатый набор конструкторов для создания эффектов.

Теория

Философия конструкторов Effect

Конструкторы Effect следуют принципу “lift” (поднятия) — они преобразуют обычные значения и вычисления в мир Effect, сохраняя информацию о типах:

┌─────────────────┐         ┌──────────────────────┐
│  Обычный мир    │  lift   │    Мир Effect        │
├─────────────────┤ ──────► ├──────────────────────┤
│ number          │         │ Effect<number>       │
│ Error           │         │ Effect<never, Error> │
│ Promise<A>      │         │ Effect<A, Error>     │
│ () => A         │         │ Effect<A>            │
└─────────────────┘         └──────────────────────┘

Классификация конструкторов

КатегорияКонструкторыПрименение
Базовыеsucceed, fail, void, unitПростые значения
Синхронныеsync, try, suspendЛенивые вычисления
Асинхронныеpromise, tryPromise, asyncPromise/callback API
Итераторыiterate, unfold, loopГенерация последовательностей
Условныеif, when, unlessУсловное выполнение
Специальныеdie, never, interruptДефекты и прерывания

Базовые конструкторы

Effect.succeed

Создаёт Effect, который немедленно успешно завершается с заданным значением.


// Сигнатура:
// succeed: <A>(value: A) => Effect<A, never, never>

const numberEffect = Effect.succeed(42)
// Effect<number, never, never>

const stringEffect = Effect.succeed("hello")
// Effect<string, never, never>

const objectEffect = Effect.succeed({ x: 1, y: 2 } as const)
// Effect<{ readonly x: 1; readonly y: 2 }, never, never>

// ⚠️ Важно: значение вычисляется СРАЗУ при создании Effect
const eager = Effect.succeed(Math.random())
// Число генерируется при создании, а не при запуске!

// Для ленивых вычислений используйте Effect.sync
const lazy = Effect.sync(() => Math.random())
// Число генерируется при каждом запуске

Effect.fail

Создаёт Effect, который немедленно завершается с типизированной ошибкой.


// Сигнатура:
// fail: <E>(error: E) => Effect<never, E, never>

// Простая ошибка
const simpleError = Effect.fail("Something went wrong")
// Effect<never, string, never>

// Типизированная ошибка через Data.TaggedError
class NetworkError extends Data.TaggedError("NetworkError")<{
  readonly url: string
  readonly status: number
}> {}

const networkError = Effect.fail(new NetworkError({ url: "/api", status: 500 }))
// Effect<never, NetworkError, never>

// Стандартный Error
const standardError = Effect.fail(new Error("Oops"))
// Effect<never, Error, never>

Effect.void

Создают Effect без полезного значения — для побочных эффектов.


// Effect.void — более современный вариант
const voidEffect: Effect.Effect<void> = Effect.void
// Effect<void, never, never>

// Использование: когда нужен "пустой" Effect в композиции
const workflow = Effect.gen(function* () {
  yield* Effect.log("Starting...")
  yield* Effect.void  // Placeholder
  yield* Effect.log("Done")
})

Effect.succeedNone и Effect.succeedSome

Создают Effect с типом Option:


// Effect.none — Effect<Option<never>, never, never>
const noneEffect = Effect.succeedNone
// При запуске вернёт Option.none()

// Effect.succeedSome — <A>(value: A) => Effect.Effect<Option<A>>
const someEffect = Effect.succeedSome(42)
// Effect<Option<number>, never, never>

// Полезно для функций поиска
const findUser = (id: string): Effect.Effect<Option.Option<User>> =>
  id === "1" 
    ? Effect.succeedSome({ id: "1", name: "John" })
    : Effect.succeedNone

Синхронные конструкторы

Effect.sync

Создаёт Effect из синхронной функции. Функция вызывается лениво при каждом запуске Effect.


// Сигнатура:
// sync: <A>(evaluate: LazyArg<A>) => Effect<A, never, never>

// Ленивое вычисление
const randomEffect = Effect.sync(() => Math.random())
// Effect<number, never, never>

// Каждый запуск даёт новое значение
await Effect.runPromise(randomEffect) // 0.7234...
await Effect.runPromise(randomEffect) // 0.1892...

// Чтение из окружения
const nowEffect = Effect.sync(() => Date.now())
// Effect<number, never, never>

// Побочные эффекты (logging, console)
const logEffect = Effect.sync(() => {
  console.log("Side effect executed!")
  return "done"
})
// Effect<string, never, never>

⚠️ Важно: Effect.sync предполагает, что функция не выбрасывает исключения. Для функций, которые могут бросить — используйте Effect.try.

Effect.try

Создаёт Effect из функции, которая может выбросить исключение. Исключение преобразуется в типизированную ошибку.


// Простая форма — ошибка типа UnknownException
const parseJson = (json: string) => Effect.try(() => JSON.parse(json))
// Effect<unknown, UnknownException, never>

// Полная форма с преобразованием ошибки
class ParseError extends Data.TaggedError("ParseError")<{
  readonly input: string
  readonly cause: unknown
}> {}

const parseJsonSafe = (json: string): Effect.Effect<unknown, ParseError> =>
  Effect.try({
    try: () => JSON.parse(json),
    catch: (error) => new ParseError({ input: json, cause: error })
  })

// Использование
const program = Effect.gen(function* () {
  const data = yield* parseJsonSafe('{"valid": true}')
  return data
})

const invalid = Effect.gen(function* () {
  const data = yield* parseJsonSafe('not json')
  return data
})
// invalid: Effect<unknown, ParseError, never>

Effect.suspend

Создаёт Effect, который вычисляет другой Effect лениво. Полезно для рекурсии и отложенного создания.


// Сигнатура:
// suspend: <A, E, R>(effect: LazyArg<Effect<A, E, R>>) => Effect<A, E, R>

// Рекурсивный Effect (без suspend была бы бесконечная рекурсия при создании)
const countdown = (n: number): Effect.Effect<void> =>
  n <= 0
    ? Effect.void
    : Effect.gen(function* () {
        yield* Effect.log(`${n}...`)
        yield* Effect.suspend(() => countdown(n - 1))
      })

// Условное создание эффекта
const conditionalEffect = (shouldFail: boolean) =>
  Effect.suspend(() =>
    shouldFail
      ? Effect.fail(new Error("Condition met"))
      : Effect.succeed("OK")
  )

// Полезно для "трамплинов" — предотвращения stack overflow
const trampoline = <A>(thunk: () => Effect.Effect<A>): Effect.Effect<A> =>
  Effect.suspend(thunk)

Effect.sync vs Effect.succeed

Важное различие для production-кода:


// ═══════════════════════════════════════════════════════════════
// Effect.succeed — значение вычисляется СРАЗУ
// ═══════════════════════════════════════════════════════════════

let counter = 0
const eager = Effect.succeed(++counter)
// counter уже равен 1!

console.log(counter) // 1
await Effect.runPromise(eager) // 1
await Effect.runPromise(eager) // 1 (то же значение)
console.log(counter) // 1

// ═══════════════════════════════════════════════════════════════
// Effect.sync — значение вычисляется ПРИ ЗАПУСКЕ
// ═══════════════════════════════════════════════════════════════

let counter2 = 0
const lazy = Effect.sync(() => ++counter2)
// counter2 всё ещё 0

console.log(counter2) // 0
await Effect.runPromise(lazy) // 1
await Effect.runPromise(lazy) // 2 (новое вычисление)
console.log(counter2) // 2

Асинхронные конструкторы

Effect.promise

Создаёт Effect из Promise. Используйте, когда Promise не может отклониться.


// Сигнатура:
// promise: <A>(evaluate: (signal: AbortSignal) => Promise<A>) => Effect<A, never, never>

// Простой fetch (предполагаем, что не падает)
const delayedValue = Effect.promise(() => 
  new Promise<number>(resolve => setTimeout(() => resolve(42), 1000))
)
// Effect<number, never, never>

// С поддержкой отмены через AbortSignal
const cancellableFetch = Effect.promise((signal) =>
  fetch("/api/data", { signal }).then(r => r.json())
)
// Effect<any, never, never>

// ⚠️ Важно: если Promise может отклониться, используйте tryPromise!

Effect.tryPromise

Создаёт Effect из Promise, который может отклониться. Отклонение преобразуется в типизированную ошибку.


// Простая форма
const fetchData = Effect.tryPromise(() => 
  fetch("/api/data").then(r => r.json())
)
// Effect<any, UnknownException, never>

// Полная форма с типизацией ошибки
class FetchError extends Data.TaggedError("FetchError")<{
  readonly url: string
  readonly cause: unknown
}> {}

const fetchDataSafe = (url: string): Effect.Effect<unknown, FetchError> =>
  Effect.tryPromise({
    try: (signal) => fetch(url, { signal }).then(r => r.json()),
    catch: (error) => new FetchError({ url, cause: error })
  })

// Практический пример
interface ApiResponse<T> {
  readonly data: T
  readonly status: number
}

class ApiError extends Data.TaggedError("ApiError")<{
  readonly endpoint: string
  readonly status: number
  readonly message: string
}> {}

const apiCall = <T>(endpoint: string): Effect.Effect<T, ApiError> =>
  Effect.tryPromise({
    try: async (signal) => {
      const response = await fetch(endpoint, { signal })
      
      if (!response.ok) {
        throw { status: response.status, message: response.statusText }
      }
      
      const json = await response.json() as ApiResponse<T>
      return json.data
    },
    catch: (error) => {
      const { status = 0, message = "Unknown error" } = error as { status?: number; message?: string }
      return new ApiError({ endpoint, status, message })
    }
  })

Effect.async

Создаёт Effect из callback-based API. Самый гибкий асинхронный конструктор.


// Сигнатура:
// async: <A, E = never, R = never>(
//   register: (callback: (_: Effect<A, E, R>) => void, signal: AbortSignal) => void | Effect<void>
// ) => Effect<A, E, R>

// Обёртка для setTimeout
const delay = (ms: number): Effect.Effect<void> =>
  Effect.async<void>((resume) => {
    const timeoutId = setTimeout(() => {
      resume(Effect.void)
    }, ms)
    
    // Cleanup при отмене (опционально)
    return Effect.sync(() => clearTimeout(timeoutId))
  })

// Обёртка для Node.js fs.readFile

const readFile = (path: string): Effect.Effect<Buffer, NodeJS.ErrnoException> =>
  Effect.async((resume) => {
    fs.readFile(path, (err, data) => {
      if (err) {
        resume(Effect.fail(err))
      } else {
        resume(Effect.succeed(data))
      }
    })
  })

// Event listener (одноразовый)
const waitForEvent = <T>(
  emitter: EventTarget,
  event: string
): Effect.Effect<T> =>
  Effect.async<T>((resume, signal) => {
    const handler = (e: Event) => {
      resume(Effect.succeed((e as CustomEvent<T>).detail))
    }
    
    emitter.addEventListener(event, handler, { once: true, signal })
    
    // Cleanup
    return Effect.sync(() => emitter.removeEventListener(event, handler))
  })

// WebSocket message
const websocketMessage = (ws: WebSocket): Effect.Effect<string, Error> =>
  Effect.async((resume, signal) => {
    const onMessage = (event: MessageEvent) => {
      resume(Effect.succeed(event.data as string))
    }
    
    const onError = (event: Event) => {
      resume(Effect.fail(new Error("WebSocket error")))
    }
    
    ws.addEventListener("message", onMessage, { once: true })
    ws.addEventListener("error", onError, { once: true })
    
    // Cleanup при отмене
    signal.addEventListener("abort", () => {
      ws.removeEventListener("message", onMessage)
      ws.removeEventListener("error", onError)
    })
  })

Effect.asyncEffect

Позволяет выполнить эффект перед регистрацией callback:


class Logger extends Context.Tag("Logger")<
  Logger,
  { readonly log: (msg: string) => Effect.Effect<void> }
>() {}

// Логируем перед ожиданием
const delayWithLog = (ms: number): Effect.Effect<void, never, Logger> =>
  Effect.asyncEffect<void, never, Logger>((resume) =>
    Effect.gen(function* () {
      const logger = yield* Logger
      yield* logger.log(`Waiting ${ms}ms...`)
      
      setTimeout(() => resume(Effect.void), ms)
    })
  )

Конструкторы из итераторов

Effect.iterate

Создаёт Effect, который итерирует, пока условие истинно:


// Сигнатура:
// iterate: <A, E, R>(
//   initial: A,
//   options: { while: (a: A) => boolean; body: (a: A) => Effect<A, E, R> }
// ) => Effect<A, E, R>

// Простой счётчик
const countTo10 = Effect.iterate(0, {
  while: (n) => n < 10,
  body: (n) => Effect.succeed(n + 1)
})
// Результат: 10

// С побочными эффектами
const countdown = Effect.iterate(5, {
  while: (n) => n > 0,
  body: (n) => Effect.gen(function* () {
    yield* Effect.log(`${n}...`)
    yield* Effect.sleep("1 second")
    return n - 1
  })
})

// Retry-логика
const retryUntilSuccess = <A, E>(
  effect: Effect.Effect<A, E>,
  maxAttempts: number
): Effect.Effect<A, E> =>
  Effect.iterate({ attempt: 0, result: null as A | null }, {
    while: (state) => state.result === null && state.attempt < maxAttempts,
    body: (state) => Effect.gen(function* () {
      const result = yield* Effect.either(effect)
      if (result._tag === "Right") {
        return { attempt: state.attempt + 1, result: result.right }
      }
      return { attempt: state.attempt + 1, result: null }
    })
  }).pipe(
    Effect.flatMap((state) =>
      state.result !== null
        ? Effect.succeed(state.result)
        : Effect.fail("Max attempts reached" as unknown as E)
    )
  )

Effect.loop

Более гибкий цикл с аккумулятором:


// Сигнатура:
// loop: <A, E, R, B>(
//   initial: A,
//   options: {
//     while: (a: A) => boolean
//     step: (a: A) => A
//     body: (a: A) => Effect<B, E, R>
//     discard?: boolean
//   }
// ) => Effect<B[], E, R>

// Генерация последовательности
const generateSequence = Effect.loop(1, {
  while: (n) => n <= 5,
  step: (n) => n + 1,
  body: (n) => Effect.succeed(n * n)
})
// Результат: [1, 4, 9, 16, 25]

// С побочными эффектами и discard
const processItems = (items: ReadonlyArray<string>) =>
  Effect.loop(0, {
    while: (i) => i < items.length,
    step: (i) => i + 1,
    body: (i) => Effect.log(`Processing: ${items[i]}`),
    discard: true  // Не собираем результаты
  })
// Результат: void (только побочные эффекты)

Условные конструкторы

Effect.if

Условный выбор между двумя эффектами:


// Сигнатура:
// if: <A1, E1, R1, A2, E2, R2>(
//   condition: boolean | Effect<boolean, E1, R1>,
//   options: { onTrue: LazyArg<Effect<A1, E1, R1>>; onFalse: LazyArg<Effect<A2, E2, R2>> }
// ) => Effect<A1 | A2, E1 | E2, R1 | R2>

const conditionalGreeting = (isVip: boolean) =>
  Effect.if(isVip, {
    onTrue: () => Effect.succeed("Welcome, VIP!"),
    onFalse: () => Effect.succeed("Hello!")
  })

// С эффективным условием
const checkAndGreet = Effect.if(
  Effect.sync(() => Math.random() > 0.5),
  {
    onTrue: () => Effect.log("Lucky!"),
    onFalse: () => Effect.log("Not this time")
  }
)

Effect.when и Effect.unless

Условное выполнение одного эффекта:


// Effect.when — выполняет если условие true
// Возвращает Option<A>

const maybeLog = (shouldLog: boolean) =>
  Effect.when(Effect.log("Conditional log"), () => shouldLog)
// Effect<Option<void>, never, never>

// Effect.unless — выполняет если условие false
const unlessDisabled = (disabled: boolean) =>
  Effect.unless(Effect.succeed("Active"), () => disabled)
// Effect<Option<string>, never, never>

// Практический пример: валидация
const validateIfNeeded = (data: string, skipValidation: boolean) =>
  Effect.gen(function* () {
    yield* Effect.unless(
      Effect.gen(function* () {
        if (data.length === 0) {
          yield* Effect.fail(new Error("Empty data"))
        }
        yield* Effect.log("Validation passed")
      }),
      () => skipValidation
    )
    
    return data
  })

Effect.whenEffect и Effect.unlessEffect

Когда условие само является эффектом:


class FeatureFlags extends Context.Tag("FeatureFlags")<
  FeatureFlags,
  { readonly isEnabled: (flag: string) => Effect.Effect<boolean> }
>() {}

const featureGatedAction = Effect.gen(function* () {
  const flags = yield* FeatureFlags
  
  yield* Effect.whenEffect(
    Effect.log("New feature activated!"),
    flags.isEnabled("new-feature")
  )
  
  return "done"
})

Специальные конструкторы

Effect.die и Effect.dieMessage

Создают Effect, который завершается дефектом (unrecoverable error):


// Effect.die — с любым значением
const defect = Effect.die(new Error("Unexpected error"))
// Effect<never, never, never>
// При запуске выбросит FiberFailure

// Effect.dieMessage — с сообщением
const defectMessage = Effect.dieMessage("Something went terribly wrong")
// Effect<never, never, never>

// Использование для assertion-подобной логики
const assertNonNull = <A>(value: A | null): Effect.Effect<A> =>
  value === null
    ? Effect.dieMessage("Assertion failed: value is null")
    : Effect.succeed(value)

// ⚠️ Дефекты НЕ отслеживаются в типе ошибки (E = never)
// Используйте только для действительно неожиданных ситуаций

Effect.never

Создаёт Effect, который никогда не завершается:


// Effect.never: Effect<never, never, never>
const neverEnds = Effect.never
// При запуске будет висеть вечно

// Практическое использование: держать процесс живым
const server = Effect.gen(function* () {
  yield* Effect.log("Server started")
  // Запускаем сервер в background
  const fiber = yield* Effect.fork(startHttpServer())
  // Держим процесс живым
  yield* Effect.never
})

// Или для типобезопасных switch-case
type Status = "pending" | "active" | "completed"

const handleStatus = (status: Status): Effect.Effect<string> => {
  switch (status) {
    case "pending": return Effect.succeed("Waiting...")
    case "active": return Effect.succeed("In progress")
    case "completed": return Effect.succeed("Done!")
    // TypeScript проверит exhaustiveness
  }
}

Effect.interrupt

Создаёт Effect, который немедленно прерывается:


const selfInterrupt = Effect.interrupt
// Effect<never, never, never>

// Условное прерывание
const interruptIf = (condition: boolean) =>
  condition ? Effect.interrupt : Effect.void

// Практический пример: graceful shutdown
const processWithShutdown = (
  shouldStop: () => boolean
): Effect.Effect<void> =>
  Effect.gen(function* () {
    while (true) {
      if (shouldStop()) {
        yield* Effect.interrupt
      }
      yield* processNextItem()
    }
  })

Effect.failCause и Effect.failCauseSync

Создают Effect с явной Cause:


// Полный контроль над Cause
const withCause = Effect.failCause(
  Cause.fail(new Error("Explicit cause"))
)
// Effect<never, Error, never>

// Параллельные ошибки
const parallelErrors = Effect.failCause(
  Cause.parallel(
    Cause.fail(new Error("First")),
    Cause.fail(new Error("Second"))
  )
)

// Последовательные ошибки
const sequentialErrors = Effect.failCause(
  Cause.sequential(
    Cause.fail(new Error("First")),
    Cause.die("Defect after error")
  )
)

Примеры

Пример 1: Обёртка для внешней библиотеки


// Предположим, есть библиотека для работы с датами
// с callback API и возможными исключениями

// Определяем типизированные ошибки
class DateParseError extends Data.TaggedError("DateParseError")<{
  readonly input: string
  readonly cause: unknown
}> {}

class DateFormatError extends Data.TaggedError("DateFormatError")<{
  readonly date: Date
  readonly format: string
}> {}

// Обёртка для parsing
const parseDate = (input: string): Effect.Effect<Date, DateParseError> =>
  Effect.try({
    try: () => {
      const date = new Date(input)
      if (isNaN(date.getTime())) {
        throw new Error("Invalid date")
      }
      return date
    },
    catch: (cause) => new DateParseError({ input, cause })
  })

// Обёртка для formatting
const formatDate = (date: Date, format: string): Effect.Effect<string, DateFormatError> =>
  Effect.try({
    try: () => {
      // Простой форматтер (в реальности использовали бы библиотеку)
      switch (format) {
        case "ISO": return date.toISOString()
        case "UTC": return date.toUTCString()
        case "LOCAL": return date.toLocaleDateString()
        default: throw new Error(`Unknown format: ${format}`)
      }
    },
    catch: () => new DateFormatError({ date, format })
  })

// Композиция
const parseAndFormat = (input: string, format: string) =>
  pipe(
    parseDate(input),
    Effect.flatMap((date) => formatDate(date, format))
  )
// Effect<string, DateParseError | DateFormatError, never>

// Использование
const program = Effect.gen(function* () {
  const formatted = yield* parseAndFormat("2024-01-15", "ISO")
  yield* Effect.log(`Formatted: ${formatted}`)
  return formatted
})

Пример 2: HTTP Client с retry


// Ошибки
class HttpError extends Data.TaggedError("HttpError")<{
  readonly url: string
  readonly status: number
  readonly body: string
}> {}

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

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

type FetchError = HttpError | NetworkError | TimeoutError

// Базовый fetch с типизацией
const fetchWithTimeout = (
  url: string,
  timeoutMs: number = 5000
): Effect.Effect<Response, FetchError> =>
  Effect.async<Response, FetchError>((resume, signal) => {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => {
      controller.abort()
      resume(Effect.fail(new TimeoutError({ url, timeoutMs })))
    }, timeoutMs)
    
    // Propagate external abort
    signal.addEventListener("abort", () => {
      controller.abort()
      clearTimeout(timeoutId)
    })
    
    fetch(url, { signal: controller.signal })
      .then((response) => {
        clearTimeout(timeoutId)
        if (!response.ok) {
          response.text().then((body) => {
            resume(Effect.fail(new HttpError({ 
              url, 
              status: response.status, 
              body 
            })))
          })
        } else {
          resume(Effect.succeed(response))
        }
      })
      .catch((error) => {
        clearTimeout(timeoutId)
        if (error.name !== "AbortError") {
          resume(Effect.fail(new NetworkError({ url, cause: error })))
        }
      })
  })

// JSON fetch с retry
const fetchJson = <T>(
  url: string,
  options?: { timeout?: number; retries?: number }
): Effect.Effect<T, FetchError> => {
  const { timeout = 5000, retries = 3 } = options ?? {}
  
  return pipe(
    fetchWithTimeout(url, timeout),
    Effect.flatMap((response) =>
      Effect.tryPromise({
        try: () => response.json() as Promise<T>,
        catch: (cause) => new NetworkError({ url, cause })
      })
    ),
    Effect.retry(
      Schedule.recurs(retries).pipe(
        Schedule.intersect(Schedule.exponential("100 millis"))
      )
    )
  )
}

// Использование
interface User {
  readonly id: number
  readonly name: string
}

const getUser = (id: number): Effect.Effect<User, FetchError> =>
  fetchJson<User>(`https://api.example.com/users/${id}`, {
    timeout: 3000,
    retries: 2
  })

Пример 3: WebSocket обёртка


class WebSocketError extends Data.TaggedError("WebSocketError")<{
  readonly type: "connection" | "message" | "close"
  readonly cause?: unknown
}> {}

interface WebSocketConnection {
  readonly send: (message: string) => Effect.Effect<void, WebSocketError>
  readonly receive: Effect.Effect<string, WebSocketError>
  readonly close: Effect.Effect<void>
}

const createWebSocket = (url: string): Effect.Effect<WebSocketConnection, WebSocketError> =>
  Effect.async<WebSocketConnection, WebSocketError>((resume) => {
    const ws = new WebSocket(url)
    const messageQueue: string[] = []
    const waiters: Array<(msg: string) => void> = []
    
    ws.onopen = () => {
      const connection: WebSocketConnection = {
        send: (message) =>
          Effect.try({
            try: () => ws.send(message),
            catch: (cause) => new WebSocketError({ type: "message", cause })
          }),
        
        receive: Effect.async<string, WebSocketError>((cb) => {
          if (messageQueue.length > 0) {
            cb(Effect.succeed(messageQueue.shift()!))
          } else {
            waiters.push((msg) => cb(Effect.succeed(msg)))
          }
        }),
        
        close: Effect.sync(() => ws.close())
      }
      
      resume(Effect.succeed(connection))
    }
    
    ws.onmessage = (event) => {
      const waiter = waiters.shift()
      if (waiter) {
        waiter(event.data)
      } else {
        messageQueue.push(event.data)
      }
    }
    
    ws.onerror = () => {
      resume(Effect.fail(new WebSocketError({ type: "connection" })))
    }
  })

// Использование
const wsProgram = Effect.gen(function* () {
  const conn = yield* createWebSocket("wss://echo.websocket.org")
  
  yield* conn.send("Hello, WebSocket!")
  const response = yield* conn.receive
  yield* Effect.log(`Received: ${response}`)
  
  yield* conn.close
})

Упражнения

Упражнение

Упражнение 2.1: Выбор конструктора

Легко

Для каждого сценария выберите подходящий конструктор:

import { Effect } from "effect"

// 1. Обернуть константу 42 в Effect
const ex1 = /* ??? */

// 2. Создать Effect из функции Math.random()
const ex2 = /* ??? */

// 3. Обернуть Promise.resolve("data") в Effect
const ex3 = /* ??? */

// 4. Создать Effect, который падает с Error("oops")
const ex4 = /* ??? */

// 5. Обернуть JSON.parse (может выбросить исключение)
const parseJson = (json: string) => /* ??? */
Упражнение

Упражнение 2.2: Async конструктор

Легко

Создайте обёртку для setTimeout:

import { Effect } from "effect"

// Реализуйте функцию delay, которая:
// 1. Возвращает Effect<void>
// 2. Ждёт указанное количество миллисекунд
// 3. Поддерживает отмену (очищает таймер)

const delay = (ms: number): Effect.Effect<void> => /* ??? */
Упражнение

Упражнение 2.3: File System обёртка

Средне

Создайте типизированную обёртку для Bun.file:

import { Effect, Data } from "effect"

// Определите ошибки
class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
  readonly path: string
}> {}

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

type FileError = FileNotFoundError | FileReadError

// Реализуйте
const readFileText = (path: string): Effect.Effect<string, FileError> => /* ??? */

const readFileJson = <T>(path: string): Effect.Effect<T, FileError> => /* ??? */
Упражнение

Упражнение 2.4: Retry с backoff

Средне

Реализуйте функцию, которая делает HTTP запрос с экспоненциальным backoff:

import { Effect, Data } from "effect"

class FetchError extends Data.TaggedError("FetchError")<{
  readonly attempt: number
  readonly cause: unknown
}> {}

// Реализуйте функцию, которая:
// 1. Делает fetch к URL
// 2. При ошибке ретраит с экспоненциальной задержкой
// 3. Возвращает JSON ответ или ошибку после maxRetries попыток

const fetchWithBackoff = <T>(
  url: string,
  maxRetries: number = 3,
  initialDelay: number = 100
): Effect.Effect<T, FetchError> => /* ??? */
Упражнение

Упражнение 2.5: Event Emitter обёртка

Сложно

Создайте полноценную обёртку для Node.js EventEmitter с поддержкой типизации:

import { Effect, Data, Queue, Fiber } from "effect"
import { EventEmitter } from "events"

// Реализуйте типизированный EventEmitter wrapper
interface TypedEmitter<Events extends Record<string, unknown>> {
  readonly emit: <K extends keyof Events>(
    event: K,
    data: Events[K]
  ) => Effect.Effect<void>

  readonly on: <K extends keyof Events>(
    event: K
  ) => Effect.Effect<Events[K]>  // Одноразовый слушатель

  readonly subscribe: <K extends keyof Events>(
    event: K
  ) => Effect.Effect<Queue.Queue<Events[K]>>  // Подписка на все события

  readonly close: Effect.Effect<void>
}

const createTypedEmitter = <Events extends Record<string, unknown>>():
  Effect.Effect<TypedEmitter<Events>> => /* ??? */

// Пример использования:
interface MyEvents {
  message: string
  error: Error
  close: void
}

const program = Effect.gen(function* () {
  const emitter = yield* createTypedEmitter<MyEvents>()

  // Подписываемся
  const messages = yield* emitter.subscribe("message")

  // Emit в background
  yield* Effect.fork(
    Effect.gen(function* () {
      yield* Effect.sleep("100 millis")
      yield* emitter.emit("message", "Hello!")
      yield* Effect.sleep("100 millis")
      yield* emitter.emit("message", "World!")
    })
  )

  // Получаем сообщения
  const msg1 = yield* Queue.take(messages)
  const msg2 = yield* Queue.take(messages)

  yield* Effect.log(`Received: ${msg1}, ${msg2}`)
  yield* emitter.close()
})

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

  1. Effect.succeed — для готовых значений (eager evaluation)
  2. Effect.sync — для ленивых синхронных вычислений
  3. Effect.try — для функций, которые могут выбросить исключение
  4. Effect.promise — для Promise, который не может отклониться
  5. Effect.tryPromise — для Promise с обработкой отклонения
  6. Effect.async — для callback-based API с поддержкой отмены
  7. Effect.suspend — для рекурсивных и отложенных Effect
  8. Выбирайте конструктор исходя из источника данных и модели ошибок

🔗 Следующая тема: Runners: runSync, runPromise, Runtime