Чистые функции

Чистая функция — функция, которая: 1) при одинаковых аргументах всегда возвращает одинаковый результат (детерминированность); 2) не имеет побочных эффектов (не изменяет внешнее состояние).

Зачем нужно

Чистые функции предсказуемы, легко тестируются, безопасно кэшируются (мемоизация) и могут выполняться параллельно. Они — основа функционального программирования и ключ к написанию надёжного кода.

Где используется

Утилиты и хелперы, Redux reducers, React-компоненты, вычисления, форматирование, валидация, преобразование данных, тесты.

Предпосылки

Function Declaration, Парадигмы

Чистая vs нечистая

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

function add(a, b) {
  return a + b; // Всегда одинаковый результат, нет побочных эффектов
}

function formatPrice(cents) {
  return `${(cents / 100).toFixed(2)} руб.`;
}

function getFullName(user) {
  return `${user.firstName} ${user.lastName}`;
}

// Детерминированность: add(2, 3) ВСЕГДА вернёт 5
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5

Нечистая функция

let total = 0;

function addToTotal(amount) {
  total += amount;     // Побочный эффект: мутация внешней переменной
  return total;
}

console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 — разный результат при одинаковом входе!

function getRandomItem(array) {
  return array[Math.floor(Math.random * array.length)]; // Недетерминированная
}

function logAndReturn(value) {
  console.log(value); // Побочный эффект: вывод в консоль
  return value;
}

Признаки чистой функции

// 1. Зависит ТОЛЬКО от аргументов
function pure(x, y) {
  return x * y + 1;
}

// 2. НЕ изменяет аргументы
function pureFilter(arr, predicate) {
  return arr.filter(predicate); // filter возвращает НОВЫЙ массив
}

// 3. НЕ обращается к внешнему изменяемому состоянию
const TAX_RATE = 0.2; // OK — константа не меняется
function calcTax(price) {
  return price * TAX_RATE;
}

// 4. НЕ производит побочных эффектов
// Нет: console.log, DOM, fetch, запись в файл, изменение глобальных переменных

Примеры чистых функций

// Форматирование
const capitalize = (str) =>
  str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();

// Вычисления
const clamp = (value, min, max) =>
  Math.min(Math.max(value, min), max);

// Преобразование данных
const extractNames = (users) =>
  users.map(u => u.name);

// Валидация
const isValidEmail = (email) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

// Создание нового объекта (не мутация)
const updateUser = (user, updates) => ({
  ...user,
  ...updates,
  updatedAt: updates.updatedAt || user.updatedAt
});

Преимущества чистых функций

Тестирование

// Чистую функцию тестировать тривиально
function calcDiscount(price, percentage) {
  return price - price * (percentage / 100);
}

// Тесты — просто вход → выход
console.assert(calcDiscount(100, 10) === 90);
console.assert(calcDiscount(200, 25) === 150);
console.assert(calcDiscount(0, 50) === 0);
// Не нужны моки, стабы, подготовка окружения

Мемоизация

// Чистые функции безопасно кэшировать
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn(...args));
    return cache.get(key);
  };
}

const expensiveCalc = memoize((n) => {
  let result = 0;
  for (let i = 0; i < n * 1e6; i++) result += Math.sqrt(i);
  return result;
});

Композиция

const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

const processUser = pipe(
  (user) => ({ ...user, name: user.name.trim() }),
  (user) => ({ ...user, email: user.email.toLowerCase() }),
  (user) => ({ ...user, isValid: isValidEmail(user.email) })
);

const result = processUser({ name: '  Иван  ', email: 'IVAN@MAIL.COM' });
// { name: 'Иван', email: 'ivan@mail.com', isValid: true }

Избегание мутации аргументов

// Плохо — мутирует входной массив
function sortBad(arr) {
  return arr.sort((a, b) => a - b); // sort мутирует!
}

// Хорошо — создаёт новый массив
function sortGood(arr) {
  return [...arr].sort((a, b) => a - b);
}

// Плохо — мутирует объект
function updateAgeBad(user, newAge) {
  user.age = newAge; // мутация!
  return user;
}

// Хорошо — новый объект
function updateAgeGood(user, newAge) {
  return { ...user, age: newAge };
}

Redux reducer — пример чистой функции

function todosReducer(state = , action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, {
        id: action.id,
        text: action.text(),
        completed: false
      }];

    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );

    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.id);

    default:
      return state;
  }
}
// Всегда возвращает НОВОЕ состояние, не мутирует старое

Частые ошибки

1. Скрытая мутация через методы массива

// sort, reverse, splice — мутируют!
const sortedBad = (arr) => arr.sort(); // мутация!
const sortedGood = (arr) => [...arr].sort(); // копия

// push, unshift — мутируют!
const addBad = (arr, item) => { arr.push(item); return arr; };
const addGood = (arr, item) => [...arr, item];

2. Date и Math.random — не чистые

// Зависимость от текущего времени
const greet = () => {
  const hour = new Date.getHours();
  return hour < 12 ? 'Доброе утро' : 'Добрый день';
};

// Чистая альтернатива — передать время как аргумент
const greetPure = (hour) =>
  hour < 12 ? 'Доброе утро' : 'Добрый день';

3. Ссылка на внешнее изменяемое состояние

let config = { locale: 'ru' };

// Нечистая — зависит от внешнего config
const formatDate = (date) => date.toLocaleDateString(config.locale);

// Чистая — locale передан явно
const formatDatePure = (date, locale) => date.toLocaleDateString(locale);

Практика

  1. Определи, какие из 5 функций чистые, а какие нет — обоснуй
  2. Перепиши нечистую функцию с мутацией массива в чистую
  3. Напиши чистый reducer для управления списком задач
  4. Создай набор чистых утилит для работы со строками
  5. Мемоизируй чистую функцию и убедись, что кэш работает корректно

Связанные темы

Ресурсы