Привет! Я Андрей - техлид в бигтехе. Слежу за фронтом, налаживаю процессы, провожу собеседования, отстреливаю плохие практики и веду телеграм-канал обо всём этом. Сегодня расшифрую монады человеческим языком. Заглядывай в канал за другими сложными концепциями в простой форме!
Около пяти лет я использую подходы из функционального стиля программирования. Чистые функции, каррирование, reduce
— всё это стало частью моего ежедневного кода. Но монады оставались для меня загадкой.
Я перечитал множество объяснений, но каждое оказывалось либо слишком абстрактным, либо чересчур упрощённым. Недавно произошло озарение: комбинация понимания Maybe
, Either
и аналогия с привычными Promise
наконец сложились в понятную картину.
Делюсь своим пониманием простым языком, без сложных абстракций и запутанных метафор.
🧩 Что такое монада по-простому?
Монада — это абстракция для цепочки действий с контекстом. Звучит сложно? Согласен, давай объясню проще:
Представь, что у тебя есть значение, но с дополнительным "контекстом":
- Значение может отсутствовать (
Maybe
) - Может случиться ошибка (
Either
) - Результат может появиться позже (
Promise
)
Каждая монада обязательно умеет делать две вещи:
-
of
— запаковать обычное значение в монаду -
chain
(он жеflatMap
) — применить функцию, которая вернёт новую монаду
Есть ещё "три закона монад" (левый идентификатор, правый идентификатор и ассоциативность), но их можно изучить позже. Главное, что благодаря им всё работает предсказуемо.
🔍 Maybe — когда значение может отсутствовать
Знакомый код?
const name = user?.profile?.name ?? 'Аноним';
Это очень удобный синтаксис. Монада Maybe реализует ту же идею, но в более явной форме:
const Maybe = value => ({
chain: fn => (value == null ? Maybe(null) : fn(value)),
map: fn => Maybe(value == null ? null : fn(value)),
fold: (none, some) => (value == null ? none() : some(value)),
inspect: () => `Maybe(${value})`
});
Maybe(user)
.map(u => u.profile)
.map(p => p.name)
.fold(() => "Аноним", name => name);
Разница лишь в том, что монадный подход делает обработку явной и универсальной, а цепочка может быть сколь угодно длинной и ветвистой
👀 Пример поинтереснее и более приближенный к реальности
👆 Развернуть
// Создаем переменные для повторного использования
const getProfile = u => u.profile;
const getSettings = p => p.settings;
const getPermissions = u => u.permissions;
const getAvatar = p => p.avatar;
// Функция для поиска пользователя по ID
// В реальном приложении это был бы запрос к API или базе данных
const findUserById = id => {
// Моковые данные для примера
const users = {
1: {
name: "Алексей",
profile: {
settings: { displayName: "Лёха" },
avatar: { url: "/images/alex.jpg" }
},
permissions: { canEdit: true },
lastLogin: "2023-05-15T10:30:00"
},
2: {
name: "Мария",
profile: {
settings: { displayName: "Маша" },
},
permissions: { canEdit: false },
}
};
// Возвращаем пользователя или null, если не найден
return users[id] || null;
};
// ID пользователя для примеров ниже
const userId = 1; // Попробуй изменить на 2 или несуществующий ID
// Создаём Maybe(userId) и используем chain для безопасного поиска пользователя
// chain применяет функцию и возвращает новую монаду, избегая вложенности
const maybeUser = Maybe(userId).chain(id => findUserById(id));
// Получаем профиль пользователя, если пользователь существует
const maybeProfile = maybeUser.map(getProfile);
// Пример 1: Получение имени пользователя с проверкой на каждом шаге
maybeProfile
.map(getSettings)
.map(s => s.displayName)
.fold(() => "Гость", name => name);
// Пример 2: Проверка прав доступа
maybeUser
.map(getPermissions)
.map(p => p.canEdit)
.fold(() => false, canEdit => canEdit);
// Пример 3: Получение URL аватара с дефолтным значением
maybeProfile
.map(getAvatar)
.map(a => a.url)
.fold(() => "/images/default-avatar.png", url => url);
// Пример 4: Цепочка с трансформацией данных
maybeUser
.map(u => u.lastLogin)
.map(date => new Date(date))
.map(d => d.toLocaleDateString())
.fold(() => "Никогда не входил", date => `Последний вход: ${date}`);
Интересно, что современные фичи JavaScript:
-
Optional Chaining
(?.
) -
Nullish Coalescing
(??
)
По сути реализуют ту же идею, что и монада Maybe
, но на уровне синтаксиса языка.
✅ Избавляет от вложенных проверок на null/undefined
✅ Делает код более декларативным и читаемым
❌ Придётся потратить пару часов на то, чтобы привыкнуть
⚔️ Either — когда нужно явно разделить успех и ошибку
Either
— это монада с двумя состояниями:
-
Right
— "правильный" результат -
Left
— ошибка или проблема
Вместо того чтобы бросать исключения или возвращать null, ты явно указываешь, что пошло не так:
Either.fromNullable(user)
.chain(u => u.age >= 18 ? Right(u) : Left("Пользователь несовершеннолетний"))
.fold(
error => console.error("Ошибка:", error),
user => console.log("Пользователь:", user)
);
👀 Реализация и приближенный к реальности пример
👆 Развернуть
// Базовая имплементация Either
const Right = value => ({
isRight: true,
isLeft: false,
map: f => Right(f(value)),
chain: f => f(value),
fold: (_, onRight) => onRight(value),
inspect: () => `Right(${value})`
});
const Left = error => ({
isRight: false,
isLeft: true,
map: _ => Left(error),
chain: _ => Left(error),
fold: (onLeft, _) => onLeft(error),
inspect: () => `Left(${error})`
});
// Вспомогательные функции
const Either = {
of: value => Right(value),
fromNullable: value => value != null ? Right(value) : Left('Value is null or undefined'),
tryCatch: (f) => {
try {
return Right(f());
} catch (e) {
return Left(e);
}
}
};
// Пример использования
const getUser = (id) => {
// Имитация запроса к базе данных
if (id === 123) {
return Either.of({ id: 123, name: "Алексей", age: 30 });
}
return Left("Пользователь не найден");
};
// Валидация с Either
const validateUser = (user) =>
user.age >= 18
? Right(user)
: Left("Пользователь несовершеннолетний");
// Цепочка операций
getUser(123)
.chain(validateUser)
.fold(
error => console.log(`Ошибка: ${error}`),
user => console.log(`Привет, ${user.name}!`)
);
✅ Делает обработку ошибок явной и предсказуемой
✅ Улучшает читаемость кода при сложной логике
❌ Названия Right и Left не очень интуитивны на первый взгляд
❌ Для некоторых сценариев может потребоваться дополнительный код
⏱️ Promise — это почти монада, и ты её уже знаешь
Интересно, что Promise
в JavaScript — очень близок к монаде:
Promise.resolve(user)
.then(u => u.profile)
.then(p => p.name)
.then(name => name ?? "Аноним")
.then(console.log)
.catch(console.error);
Метод .then()
здесь выполняет роль монадического chain
. А .catch()
работает как обработчик ошибочного пути из Either
.
Главное отличие в том, что Promise
работает асинхронно и автоматически оборачивает возвращаемые значения.
💡 Что мне дали монады в реальной работе?
Когда я наконец разобрался с монадами, то обнаружил, что они решают вполне практические задачи:
- ✅ Упрощают сложные цепочки операций с проверками
- ✅ Делают код более предсказуемым и безопасным
- ✅ Снижают количество ветвлений и вложенных условий
На практике многие из нас уже используют монадоподобные подходы:
- Цепочки
.then()
для промисов - Операторы
?.
и??
для безопасного доступа к свойствам
🌟 Монады на пальцах
Вот как бы я теперь объяснил монады:
"Монада — это способ связывать последовательные вычисления, особенно когда на каждом шаге что-то может пойти не так."
Если интересно дальше разбираться в концепциях полезных в повседневной работе — подписывайся на мой телеграм-канал
Top comments (0)