Monad Pattern: основы

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

Зачем нужно

Монады позволяют цепочкой применять функции к значению, когда каждый шаг может «провалиться» или вернуть обёрнутый результат, без вложенных if и try/catch. Это делает код декларативным и легко компонуемым. Promise в JavaScript — монада для асинхронных операций.

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

  • Promise: цепочка .then / .catch — монада для асинхронности
  • Maybe/Option — безопасная работа с null/undefined
  • Either/Result — обработка ошибок без try/catch
  • Функциональные библиотеки: fp-ts, Ramda

Основной контент

Promise как монада

// Promise — монада: wrap (Promise.resolve), flatMap (.then)
Promise.resolve(5)
  .then(x => x * 2)          // 10
  .then(x => x + 1)          // 11
  .then(x => Promise.resolve(x * 3)) // flatMap (не вложено!)
  .then(console.log);        // 33

Maybe монада

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

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

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

  // map: применить функцию если значение есть
  map(fn) {
    if (this.isNothing) return this;
    return Maybe.of(fn(this._value));
  }

  // flatMap: fn возвращает Maybe
  flatMap(fn) {
    if (this.isNothing) return this;
    return fn(this._value);
  }

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

// Безопасная работа с вложенными объектами
const user = { address: { city: { name: 'Москва' } } };
const noCity = { address: null };

const getCityName = (user) =>
  Maybe.of(user)
    .map(u => u.address)
    .map(a => a.city)
    .map(c => c.name)
    .getOrElse('Неизвестно');

console.log(getCityName(user));   // 'Москва'
console.log(getCityName(noCity)); // 'Неизвестно' (без ошибки!)

Either монада (Result)

class Right {
  constructor(value) { this._value = value; }
  map(fn) { return new Right(fn(this._value)); }
  flatMap(fn) { return fn(this._value); }
  fold(_, onRight) { return onRight(this._value); }
}

class Left {
  constructor(error) { this._error = error; }
  map(_) { return this; }       // ошибка пропускает map
  flatMap(_) { return this; }
  fold(onLeft, _) { return onLeft(this._error); }
}

const Either = {
  right: v => new Right(v),
  left: e => new Left(e),
  tryCatch: (fn) => {
    try { return Either.right(fn); }
    catch (e) { return Either.left(e.message); }
  }
};

const parseJSON = (str) =>
  Either.tryCatch( => JSON.parse(str));

parseJSON('{"a":1}')
  .map(obj => obj.a)
  .fold(
    err => console.error('Ошибка:', err),
    val => console.log('Значение:', val)  // 1
  );

parseJSON('invalid json')
  .map(obj => obj.a)
  .fold(
    err => console.error('Ошибка:', err), // 'Unexpected token...'
    val => console.log('Значение:', val)
  );

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

  • Монада != функтор — функтор только map, монада добавляет flatMap для предотвращения Maybe<Maybe<T>>. Без flatMap вложенные обёртки накапливаются.
  • Избыточное применение — монады полезны при работе с ошибками и null, но усложняют простой код. Используйте там, где реально нужна цепочка с возможным «провалом».
  • Путаница с Promise — Promise не строгая монада (не следует всем законам), но на практике работает как монада для асинхронности.

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

Ресурсы