Монады: основы для 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']
Три закона монад
- Left identity:
Maybe.of(x).chain(f)===f(x) - Right identity:
m.chain(Maybe.of)===m - 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 ?? 'Город не указан';