Привет! Я Андрей - техлид в бигтехе. Слежу за фронтом, налаживаю процессы, провожу собеседования, отстреливаю плохие практики и веду телеграм-канал обо всём этом. Сегодня расшифрую монады человеческим языком. Заглядывай в канал за другими сложными концепциями в простой форме!


Около пяти лет я использую подходы из функционального стиля программирования. Чистые функции, каррирование, reduce — всё это стало частью моего ежедневного кода. Но монады оставались для меня загадкой.

Я перечитал множество объяснений, но каждое оказывалось либо слишком абстрактным, либо чересчур упрощённым. Недавно произошло озарение: комбинация понимания Maybe, Either и аналогия с привычными Promise наконец сложились в понятную картину.

Делюсь своим пониманием простым языком, без сложных абстракций и запутанных метафор.


🧩 Что такое монада по-простому?

Монада — это абстракция для цепочки действий с контекстом. Звучит сложно? Согласен, давай объясню проще:

Представь, что у тебя есть значение, но с дополнительным "контекстом":

  • Значение может отсутствовать (Maybe)
  • Может случиться ошибка (Either)
  • Результат может появиться позже (Promise)

Каждая монада обязательно умеет делать две вещи:

  1. of — запаковать обычное значение в монаду
  2. 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() для промисов
  • Операторы ?. и ?? для безопасного доступа к свойствам

🌟 Монады на пальцах

Вот как бы я теперь объяснил монады:

"Монада — это способ связывать последовательные вычисления, особенно когда на каждом шаге что-то может пойти не так."


Если интересно дальше разбираться в концепциях полезных в повседневной работе — подписывайся на мой телеграм-канал