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 не строгая монада (не следует всем законам), но на практике работает как монада для асинхронности.