Монады: основы для JS-разработчика

Монада — паттерн функционального программирования: обёртка над значением с методом chain (или flatMap), позволяющая последовательно применять функции к обёрнутому значению, не разворачивая обёртку вручную.

Зачем нужно

Монады позволяют писать цепочки преобразований данных в явном, декларативном стиле без вложенных if-проверок и разбросанной обработки ошибок. JavaScript-разработчики сталкиваются с монадами постоянно: Promise, Array.flatMap, Optional — всё это монадические паттерны.

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

  • Обработка null/undefined (Maybe/Option монада)
  • Цепочки асинхронных операций (Promise — это монада)
  • Обработка ошибок без try/catch (Either/Result монада)
  • Функциональные библиотеки: fp-ts, ramda, folktale

Promise как монада

Promise — наиболее знакомая монада в JS:

// chain = .then (flatMap для Promise)
fetch('/api/user')
  .then(res => res.json())          // преобразование
  .then(user => fetchProfile(user.id)) // возвращает Promise — flatMap
  .then(profile => console.log(profile))
  .catch(err => console.error(err));

Maybe монада (обработка null/undefined)

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

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

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

  map(fn) {
    if (this.isNothing) return this;
    return Maybe.of(fn(this._value));
  }

  // flatMap / chain — принимает функцию, возвращающую Maybe
  chain(fn) {
    if (this.isNothing) return this;
    return fn(this._value);
  }

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

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

const getCity = (u) =>
  Maybe.of(u)
    .chain(u => Maybe.of(u.address))
    .map(addr => addr.city)
    .getOrElse('Город не указан');

console.log(getCity(user));              // 'Москва'
console.log(getCity(userWithoutAddress)); // 'Город не указан'

Either монада (обработка ошибок)

class Right {
  constructor(value) { this._value = value; }
  map(fn) { return new Right(fn(this._value)); }
  chain(fn) { return fn(this._value); }
  fold(leftFn, rightFn) { return rightFn(this._value); }
}

class Left {
  constructor(value) { this._value = value; }
  map(_fn) { return this; } // ошибка — пропускаем
  chain(_fn) { return this; }
  fold(leftFn, _rightFn) { return leftFn(this._value); }
}

const tryCatch = (fn) => {
  try { return new Right(fn); }
  catch (e) { return new Left(e.message); }
};

const parseJSON = (str) =>
  tryCatch( => JSON.parse(str))
    .map(data => data.name)
    .fold(
      err => `Ошибка: ${err}`,
      name => `Имя: ${name}`
    );

console.log(parseJSON('{"name":"Анна"}')); // Имя: Анна
console.log(parseJSON('невалидный json'));   // Ошибка: Unexpected token...

Array как монада (flatMap)

// Array.flatMap — это тоже chain/bind
const sentences = ['привет мир', 'foo bar'];

const words = sentences.flatMap(s => s.split(' '));
console.log(words); // ['привет', 'мир', 'foo', 'bar']

Три закона монад

  1. Left identity: Maybe.of(x).chain(f) === f(x)
  2. Right identity: m.chain(Maybe.of) === m
  3. Associativity: m.chain(f).chain(g) === m.chain(x => f(x).chain(g))

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

1. Путаница map и chain

// map — для функций, возвращающих обычное значение
Maybe.of(5).map(x => x * 2); // Maybe(10)

// chain — для функций, возвращающих Maybe (избегаем Maybe(Maybe(x)))
Maybe.of(5).chain(x => Maybe.of(x * 2)); // Maybe(10)
// НЕ делай:
Maybe.of(5).map(x => Maybe.of(x * 2)); // Maybe(Maybe(10)) — двойная обёртка

2. Излишнее усложнение

Монады полезны в контексте функционального стиля. В ООП-коде они могут быть избыточны — используй ?. (optional chaining) для простых случаев:

const city = user?.address?.city ?? 'Город не указан';

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

Ресурсы