Конструкторы 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, async | Promise/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) => /* ??? */import { Effect } from "effect"
// 1. Effect.succeed — для константы
const ex1 = Effect.succeed(42)
// 2. Effect.sync — для ленивой функции
const ex2 = Effect.sync(() => Math.random())
// 3. Effect.promise — для Promise
const ex3 = Effect.promise(() => Promise.resolve("data"))
// 4. Effect.fail — для типизированной ошибки
const ex4 = Effect.fail(new Error("oops"))
// 5. Effect.try — для функции, которая может выбросить
const parseJson = (json: string) => Effect.try(() => JSON.parse(json))Упражнение 2.2: Async конструктор
Создайте обёртку для setTimeout:
import { Effect } from "effect"
// Реализуйте функцию delay, которая:
// 1. Возвращает Effect<void>
// 2. Ждёт указанное количество миллисекунд
// 3. Поддерживает отмену (очищает таймер)
const delay = (ms: number): Effect.Effect<void> => /* ??? */import { Effect } from "effect"
const delay = (ms: number): Effect.Effect<void> =>
Effect.async<void>((resume, signal) => {
const timeoutId = setTimeout(() => {
resume(Effect.void)
}, ms)
// Cleanup при отмене
signal.addEventListener("abort", () => {
clearTimeout(timeoutId)
})
// Альтернативно, можно вернуть Effect для cleanup
return Effect.sync(() => clearTimeout(timeoutId))
})
// Тест
const program = Effect.gen(function* () {
yield* Effect.log("Starting...")
yield* delay(1000)
yield* Effect.log("Done!")
})
Effect.runPromise(program)Упражнение 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> => /* ??? */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> =>
Effect.gen(function* () {
const file = Bun.file(path)
// Проверяем существование
const exists = yield* Effect.promise(() => file.exists())
if (!exists) {
return yield* Effect.fail(new FileNotFoundError({ path }))
}
// Читаем содержимое
return yield* Effect.tryPromise({
try: () => file.text(),
catch: (cause) => new FileReadError({ path, cause })
})
})
const readFileJson = <T>(path: string): Effect.Effect<T, FileError> =>
Effect.gen(function* () {
const file = Bun.file(path)
const exists = yield* Effect.promise(() => file.exists())
if (!exists) {
return yield* Effect.fail(new FileNotFoundError({ path }))
}
return yield* Effect.tryPromise({
try: () => file.json() as Promise<T>,
catch: (cause) => new FileReadError({ path, cause })
})
})
// Тест
const program = Effect.gen(function* () {
const config = yield* readFileJson<{ port: number }>("./config.json")
yield* Effect.log(`Port: ${config.port}`)
})Упражнение 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> => /* ??? */import { Effect, Data, pipe } from "effect"
class FetchError extends Data.TaggedError("FetchError")<{
readonly attempt: number
readonly cause: unknown
}> {}
const fetchWithBackoff = <T>(
url: string,
maxRetries: number = 3,
initialDelay: number = 100
): Effect.Effect<T, FetchError> =>
Effect.iterate(
{ attempt: 0, result: null as T | null, delay: initialDelay },
{
while: (state) => state.result === null && state.attempt <= maxRetries,
body: (state) =>
pipe(
// Ждём перед повторной попыткой (кроме первой)
state.attempt > 0
? Effect.sleep(`${state.delay} millis`)
: Effect.void,
Effect.flatMap(() =>
Effect.tryPromise({
try: async () => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json() as Promise<T>
},
catch: (cause) => ({ cause })
})
),
Effect.match({
onSuccess: (result) => ({
attempt: state.attempt + 1,
result,
delay: state.delay
}),
onFailure: () => ({
attempt: state.attempt + 1,
result: null,
delay: state.delay * 2 // Экспоненциальный backoff
})
})
)
}
).pipe(
Effect.flatMap((state) =>
state.result !== null
? Effect.succeed(state.result)
: Effect.fail(new FetchError({
attempt: state.attempt,
cause: `Failed after ${maxRetries} retries`
}))
)
)
// Тест
const program = Effect.gen(function* () {
const data = yield* fetchWithBackoff<{ message: string }>(
"https://api.example.com/data",
3,
100
)
yield* Effect.log(`Received: ${data.message}`)
})Упражнение 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()
})import { Effect, Data, Queue, Fiber, pipe, Ref } from "effect"
import { EventEmitter } from "events"
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>> =>
Effect.gen(function* () {
const emitter = new EventEmitter()
const subscriptions = yield* Ref.make<Map<string, Queue.Queue<unknown>>>(new Map())
const instance: TypedEmitter<Events> = {
emit: <K extends keyof Events>(event: K, data: Events[K]) =>
Effect.gen(function* () {
// Emit to native emitter
emitter.emit(event as string, data)
// Also push to any subscribed queues
const subs = yield* Ref.get(subscriptions)
const queue = subs.get(event as string)
if (queue) {
yield* Queue.offer(queue, data)
}
}),
on: <K extends keyof Events>(event: K) =>
Effect.async<Events[K]>((resume) => {
const handler = (data: Events[K]) => {
resume(Effect.succeed(data))
}
emitter.once(event as string, handler)
return Effect.sync(() => {
emitter.off(event as string, handler)
})
}),
subscribe: <K extends keyof Events>(event: K) =>
Effect.gen(function* () {
const queue = yield* Queue.unbounded<Events[K]>()
yield* Ref.update(subscriptions, (map) => {
const newMap = new Map(map)
newMap.set(event as string, queue as Queue.Queue<unknown>)
return newMap
})
return queue
}),
close: Effect.gen(function* () {
const subs = yield* Ref.get(subscriptions)
for (const queue of subs.values()) {
yield* Queue.shutdown(queue)
}
yield* Ref.set(subscriptions, new Map())
emitter.removeAllListeners()
})
}
return instance
})
// Тест
interface MyEvents {
message: string
error: Error
close: void
}
const program = Effect.gen(function* () {
const emitter = yield* createTypedEmitter<MyEvents>()
const messages = yield* emitter.subscribe("message")
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()
})
Effect.runPromise(program)Ключевые выводы
- Effect.succeed — для готовых значений (eager evaluation)
- Effect.sync — для ленивых синхронных вычислений
- Effect.try — для функций, которые могут выбросить исключение
- Effect.promise — для Promise, который не может отклониться
- Effect.tryPromise — для Promise с обработкой отклонения
- Effect.async — для callback-based API с поддержкой отмены
- Effect.suspend — для рекурсивных и отложенных Effect
- Выбирайте конструктор исходя из источника данных и модели ошибок
🔗 Следующая тема: Runners: runSync, runPromise, Runtime