Чистые функции и побочные эффекты
Чистая функция
Функция, которая:
- При одинаковых аргументах возвращает одинаковый результат
- Не имеет побочных эффектов
// Чистая
const add = (a, b) => a + b;
add(1, 2); // всегда 3
// Не чистая — зависит от внешнего
let tax = 0.2;
const withTax = (price) => price * (1 + tax);
Побочные эффекты
Всё, что меняет состояние вне функции:
- Изменение глобальных переменных
- Мутация аргументов
- Запись в файл, БД
- HTTP-запросы
console.logDate.now(),Math.random()- Работа с DOM
// Не чистая — мутирует аргумент
function addItem(arr, item) {
arr.push(item);
return arr;
}
// Чистая
function addItem(arr, item) {
return [...arr, item];
}
Почему важны
- Тестирование — не нужны моки
- Отладка — легче найти ошибку
- Кеширование — результат можно запомнить
- Параллельность — не мешают друг другу
- Читаемость — понятно, что делает функция
Пример: нечистая
const users = [];
function addUser(name) {
users.push({ id: users.length + 1, name });
return users;
}
Проблемы:
- Зависит от внешнего массива
- Мутирует его
- Нельзя вызвать дважды с тем же результатом
Пример: чистая
function addUser(users, name) {
return [...users, { id: users.length + 1, name }];
}
Плюсы:
- Все данные — в аргументах
- Не мутирует
- Легко тестировать
Композиция чистых
const numbers = [1, 2, 3, 4, 5];
const result = numbers
.filter((n) => n % 2 === 0)
.map((n) => n * 2)
.reduce((a, b) => a + b, 0);
Каждый шаг — чистая функция. Результат предсказуем.
Побочные эффекты неизбежны
Чистое ядро + границы с эффектами:
// Чистая логика
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Эффект на границе
async function checkout(items) {
const total = calculateTotal(items);
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ items, total }),
});
}
calculateTotal легко тестируется. checkout — тонкая обёртка с эффектом.
Идемпотентность
Функция идемпотентна, если повторный вызов с теми же аргументами даёт тот же результат и то же состояние:
// Идемпотентная
function setTrue(obj) {
obj.active = true;
}
// Не идемпотентная
function increment(obj) {
obj.count++;
}
Функциональное ядро, императивная оболочка
Паттерн:
- Чистая логика в отдельных функциях
- Побочные эффекты — на границе
// Ядро
function validate(data) { /* чистая */ }
function transform(data) { /* чистая */ }
// Оболочка
async function saveUser(input) {
const errors = validate(input);
if (errors.length) return { errors };
const data = transform(input);
await db.users.insert(data);
return { data };
}
Тестирование
Чистые функции:
import { describe, it, expect } from 'vitest';
describe('add', () => {
it('складывает', () => {
expect(add(1, 2)).toBe(3);
});
});
Нечистые — нужны моки, стабы, чистка состояния.
Мемоизация чистых
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
// Работает только если fn чистая
const memoizedSquare = memoize((n) => n * n);
Мемоизировать нечистую функцию бессмысленно.
Итоги
- Чистая функция — без побочных эффектов
- Одинаковый вход — одинаковый выход
- Легче тестировать и отлаживать
- Побочные эффекты — на границе
- Чистое ядро + оболочка с эффектами
- Мемоизация только для чистых