Урок 09

Чистые функции и побочные эффекты

Чистая функция

Функция, которая:

  1. При одинаковых аргументах возвращает одинаковый результат
  2. Не имеет побочных эффектов
// Чистая
const add = (a, b) => a + b;
add(1, 2); // всегда 3

// Не чистая — зависит от внешнего
let tax = 0.2;
const withTax = (price) => price * (1 + tax);

Побочные эффекты

Всё, что меняет состояние вне функции:

  • Изменение глобальных переменных
  • Мутация аргументов
  • Запись в файл, БД
  • HTTP-запросы
  • console.log
  • Date.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);

Мемоизировать нечистую функцию бессмысленно.

Итоги

  • Чистая функция — без побочных эффектов
  • Одинаковый вход — одинаковый выход
  • Легче тестировать и отлаживать
  • Побочные эффекты — на границе
  • Чистое ядро + оболочка с эффектами
  • Мемоизация только для чистых