Функциональное программирование в JavaScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в JavaScript: от теории к практике на Node.js

Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как вычисление математических функций и избегает изменения состояния и изменяемых данных. В контексте JavaScript и Node.js ФП позволяет писать более предсказуемый, тестируемый и легко поддерживаемый код, особенно при работе с асинхронными потоками и сложными трансформациями данных. Эта статья предназначена для разработчиков, уже знакомых с основами ФП, и углубляется в продвинутые концепции: композицию функций, функторы, монады и управление побочными эффектами.

1. Чистые функции и иммутабельность: фундамент надежности

На продвинутом уровне недостаточно просто знать определение чистой функции. Важно понимать, как иммутабельность и отсутствие побочных эффектов влияют на архитектуру приложения, особенно в среде Node.js, где часто используются конкурентные операции.

1.1. Глубокое копирование и структурный обмен (Structural Sharing)

Полное глубокое копирование больших объектов может быть дорогим. Вместо этого библиотеки вроде Immer или Immutable.js используют структурный обмен: при изменении части объекта создается новая ссылка, но неизмененные части разделяются с предыдущим состоянием.

// Пример с использованием Immer (популярная библиотека)
const { produce } = require('immer');

const baseState = {
  user: { name: 'Alice', age: 30 },
  meta: { created: Date.now() }
};

// produce создает новое состояние, не мутируя baseState
const nextState = produce(baseState, draft => {
  draft.user.age = 31;
});

console.log(baseState.user.age); // 30
console.log(nextState.user.age); // 31
console.log(baseState.meta === nextState.meta); // true (Structural Sharing)

1.2. Чистые функции и мемоизация

Чистые функции идеально подходят для мемоизации — кэширования результатов для предотвращения повторных вычислений. В Node.js это критично для тяжелых вычислений или запросов к БД.

// Простая мемоизация для чистой функции
const memoize = (fn) => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('Из кэша');
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

const fibonacci = (n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2));
const memoizedFib = memoize(fibonacci);

console.log(memoizedFib(40)); // Вычисляется
console.log(memoizedFib(40)); // Из кэша

2. Композиция функций: строим сложное из простого

Композиция — это процесс объединения двух или более функций для получения новой функции. На продвинутом уровне мы используем не только простую композицию, но и такие паттерны, как Pipe и Compose, которые являются основой для построения пайплайнов обработки данных.

2.1. Pipe и Compose

Pipe выполняет функции слева направо, а Compose — справа налево. В Node.js это часто используется для обработки входящих данных (например, запросов HTTP).

// Реализация pipe
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

// Пример: обработка строки
const toLowerCase = (str) => str.toLowerCase();
const removeSpaces = (str) => str.replace(/\s+/g, '-');
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);

const slugify = pipe(toLowerCase, removeSpaces, capitalize);
console.log(slugify('Hello World From Node.js')); // Hello-world-from-node.js

2.2. Асинхронные пайплайны

В реальных приложениях Node.js функции часто асинхронны. Создадим версию pipe для работы с промисами.

const asyncPipe = (...fns) => (x) =>
  fns.reduce((acc, fn) => acc.then(fn), Promise.resolve(x));

// Асинхронные функции
const fetchUser = async (id) => ({ id, name: 'Bob' });
const fetchPosts = async (user) => ({ ...user, posts: ['Post1', 'Post2'] });
const formatResponse = async (data) => JSON.stringify(data, null, 2);

const getUserProfile = asyncPipe(fetchUser, fetchPosts, formatResponse);

getUserProfile(1).then(console.log); // {"id":1,"name":"Bob","posts":["Post1","Post2"]}

3. Функторы и монады: управление контекстом и побочными эффектами

Функторы и монады — это абстракции, которые позволяют работать со значениями внутри некоторого контекста (например, Maybe для null, Either для ошибок, IO для побочных эффектов). В JavaScript они не встроены, но легко реализуются.

3.1. Функтор Maybe (Option)

Функтор — это контейнер, который реализует метод map. Maybe помогает избежать ошибок с null или undefined.

class Maybe {
  constructor(value) {
    this._value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  map(fn) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value));
  }

  isNothing() {
    return this._value === null || this._value === undefined;
  }

  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this._value;
  }
}

// Использование
const user = { name: 'Alice', address: { city: 'Wonderland' } };

const city = Maybe.of(user)
  .map(u => u.address)
  .map(a => a.city)
  .getOrElse('Unknown');

console.log(city); // Wonderland

const noCity = Maybe.of({ name: 'Bob' })
  .map(u => u.address)
  .map(a => a.city)
  .getOrElse('Unknown');

console.log(noCity); // Unknown (без ошибки)

3.2. Монада Either для обработки ошибок

Монада — это функтор, который также реализует метод flatMap (или chain). Either позволяет обрабатывать ошибки без try/catch, сохраняя функциональный стиль.

class Either {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  static Left(value) {
    return new Either(value, null);
  }

  static Right(value) {
    return new Either                

Блоги

Рекомендаций по книгам