Что такое Effect-ts и зачем он нужен?
Effect-ts — это мощная библиотека для TypeScript, которая привносит в язык концепции функционального программирования, позволяя писать чистый, композируемый и типобезопасный код. Она решает одну из главных проблем в разработке на TypeScript (и JavaScript в целом) — управление побочными эффектами (side effects).
В традиционном императивном программировании побочные эффекты (работа с сетью, файлами, базами данных, генерация случайных чисел, работа с временем) разбросаны по всему коду, что делает его сложным для тестирования, отладки и анализа. Effect-ts предлагает элегантный подход: все побочные эффекты описываются как данные (значения), которые можно комбинировать, трансформировать и выполнять в контролируемой среде.
Основные преимущества Effect-ts:
- Типобезопасность: Компилятор TypeScript проверяет типы ошибок и успешных результатов на уровне типов.
- Композируемость: Эффекты легко комбинируются с помощью функций map, flatMap, zip и других.
- Управление ошибками: Ошибки обрабатываются как часть типа эффекта, а не через исключения.
- Тестируемость: Зависимости (например, HTTP-клиенты, базы данных) можно легко подменять.
- Конкурентность: Встроенная поддержка асинхронности, параллелизма и отмены операций.
- Предсказуемость: Код становится более декларативным и легче для понимания.
Effect-ts особенно полезен в крупных проектах, где важна надежность, тестируемость и масштабируемость. Он используется в бэкенд-разработке (Node.js), фронтенде (React) и даже в мобильных приложениях.
Установка
Для установки Effect-ts в ваш проект используйте npm или yarn:
npm install effect
или
yarn add effect
Также убедитесь, что у вас установлен TypeScript версии 4.7 или выше:
npm install typescript --save-dev
Для работы с Effect-ts вам понадобится настроить tsconfig.json, включив строгий режим (strict: true) и, желательно, target: "ES2020" или выше.
Быстрый старт с примером кода
Давайте напишем простую программу, которая считывает имя пользователя из командной строки и выводит приветствие. Это покажет базовую концепцию Effect-ts: описание эффекта и его выполнение.
import { Effect, Console } from "effect"
// Описываем эффект: запрос имени
const askName: Effect.Effect<string> = Effect.sync(() => {
// В реальном приложении здесь был бы ввод с клавиатуры
return "Алексей"
})
// Описываем эффект: вывод приветствия
const greet = (name: string): Effect.Effect<void> =>
Console.log(`Привет, ${name}!`)
// Комбинируем эффекты
const program: Effect.Effect<void> = askName.pipe(
Effect.flatMap(greet)
)
// Запускаем программу
Effect.runSync(program)
// Вывод: Привет, Алексей!
В этом примере:
Effect.sync— создает эффект из синхронной функции.Console.log— это эффект для вывода в консоль.pipe— позволяет последовательно применять функции к значению.flatMap— используется для последовательного выполнения эффектов.Effect.runSync— выполняет эффект синхронно (только для синхронных эффектов).
Основные методы и функции Effect-ts
1. Effect.sync
Что делает: Создает эффект, который оборачивает синхронную функцию. Функция выполняется лениво (только при запуске эффекта).
Параметры:
f: () => A— синхронная функция, возвращающая значение типа A.
Пример:
import { Effect } from "effect"
const getRandomInt: Effect.Effect<number> = Effect.sync(() => {
return Math.floor(Math.random() * 100)
})
const result = Effect.runSync(getRandomInt)
console.log(result) // Вывод: случайное число от 0 до 99
2. Effect.async
Что делает: Создает эффект из асинхронной операции (например, callback-based API). Позволяет интегрировать существующий асинхронный код.
Параметры:
register: (resume: (effect: Effect<A, E>) => void) => void— функция, которая получает функцию resume для завершения эффекта.
Пример:
import { Effect } from "effect"
const readFileAsync = (path: string): Effect.Effect<string> =>
Effect.async<string>((resume) => {
// Имитация асинхронного чтения файла
setTimeout(() => {
resume(Effect.succeed(`Содержимое файла: ${path}`))
}, 1000)
})
const program = readFileAsync("data.txt")
const result = await Effect.runPromise(program)
console.log(result) // Вывод: Содержимое файла: data.txt
3. Effect.map
Что делает: Трансформирует успешное значение эффекта с помощью функции. Аналог Array.map для эффектов.
Параметры:
f: (a: A) => B— функция преобразования значения.
Пример:
import { Effect } from "effect"
const getNumber: Effect.Effect<number> = Effect.sync(() => 42)
const toString: Effect.Effect<string> = getNumber.pipe(
Effect.map((n) => `Число: ${n}`)
)
console.log(Effect.runSync(toString)) // Вывод: Число: 42
4. Effect.flatMap
Что делает: Последовательно выполняет два эффекта, где второй зависит от результата первого. Ключевая функция для композиции эффектов.
Параметры:
f: (a: A) => Effect<B, E, R>— функция, принимающая значение и возвращающая новый эффект.
Пример:
import { Effect, Console } from "effect"
const getUserName: Effect.Effect<string> = Effect.sync(() => "Иван")
const saveToDatabase = (name: string): Effect.Effect<void> =>
Console.log(`Сохранено: ${name}`)
const program = getUserName.pipe(
Effect.flatMap((name) => saveToDatabase(name))
)
Effect.runSync(program) // Вывод: Сохранено: Иван
5. Effect.zip
Что делает: Объединяет два эффекта в один, возвращая кортеж из результатов. Выполняет эффекты последовательно.
Параметры:
that: Effect<B, E, R>— второй эффект.- Опционально:
options?: { concurrent?: boolean }— для параллельного выполнения.
Пример:
import { Effect } from "effect"
const getX: Effect.Effect<number> = Effect.sync(() => 10)
const getY: Effect.Effect<number> = Effect.sync(() => 20)
const combined: Effect.Effect<[number, number]> = Effect.zip(getX, getY)
const [x, y] = Effect.runSync(combined)
console.log(x, y) // Вывод: 10 20
6. Effect.all
Что делает: Объединяет массив или объект эффектов в один эффект, возвращающий массив или объект результатов. По умолчанию выполняется последовательно.
Параметры:
effects: Iterable<Effect<A, E, R>> | Record<string, Effect<A, E, R>>— коллекция эффектов.- Опционально:
options?: { concurrency?: number | "unbounded" }— уровень параллелизма.
Пример:
import { Effect } from "effect"
const effects = [
Effect.sync(() => 1),
Effect.sync(() => 2),
Effect.sync(() => 3),
]
const allNumbers: Effect.Effect<number[]> = Effect.all(effects)
console.log(Effect.runSync(allNumbers)) // Вывод: [1, 2, 3]
7. Effect.catchAll
Что делает: Обрабатывает ошибки эффекта, предоставляя альтернативный эффект для восстановления.
Параметры:
handler: (e: E) => Effect<A, E2, R>— функция обработки ошибки.
Пример:
import { Effect, Console } from "effect"
const riskyOperation: Effect.Effect<number, string> = Effect.sync(() => {
throw new Error("Что-то пошло не так")
})
const safeOperation = riskyOperation.pipe(
Effect.catchAll((error) => {
Console.log(`Ошибка: ${error}`)
return Effect.succeed(0) // Возвращаем значение по умолчанию
})
)
console.log(Effect.runSync(safeOperation)) // Вывод: Ошибка: Что-то пошло не так \n 0
8. Effect.tryCatch
Что делает: Создает эффект, который перехватывает исключения из синхронной функции и преобразует их в ошибку эффекта.
Параметры:
attempt: () => A— функция, которая может выбросить исключение.catch: (u: unknown) => E— функция преобразования исключения в ошибку.
Пример:
import { Effect } from "effect"
const parseJson = (json: string): Effect.Effect<unknown, Error> =>
Effect.tryCatch(
() => JSON.parse(json),
(error) => new Error(`Ошибка парсинга: ${error}`)
)
const validJson = parseJson('{"name": "Анна"}')
console.log(Effect.runSync(validJson)) // Вывод: { name: 'Анна' }
const invalidJson = parseJson("не json")
console.log(Effect.runSync(invalidJson)) // Ошибка: Error: Ошибка парсинга: ...
9. Effect.retry
Что делает: Повторяет выполнение эффекта при возникновении ошибки. Можно настроить количество попыток и стратегию ожидания.
Параметры:
policy: Schedule<A, E, Out>— расписание повторных попыток.
Пример:
import { Effect, Schedule, Console } from "effect"
let attempts = 0
const unstableOperation: Effect.Effect<string> = Effect.sync(() => {
attempts++
if (attempts < 3) {
throw new Error("Временная ошибка")
}
return "Успех!"
})
const program = unstableOperation.pipe(
Effect.retry(Schedule.recurs(3)), // Повторить до 3 раз
Effect.catchAll((e) => {
Console.log(`Не удалось после всех попыток: ${