DEV Community

Cover image for Я наконец понял, что такое монады (и ты сейчас тоже поймёшь)
Nubami SQReder
Nubami SQReder

Posted on

Я наконец понял, что такое монады (и ты сейчас тоже поймёшь)

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


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

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

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


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

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

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

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

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

  1. of — запаковать обычное значение в монаду
  2. chain (он же flatMap) — применить функцию, которая вернёт новую монаду

Есть ещё "три закона монад" (левый идентификатор, правый идентификатор и ассоциативность), но их можно изучить позже. Главное, что благодаря им всё работает предсказуемо.


🔍 Maybe — когда значение может отсутствовать

Знакомый код?

const name = user?.profile?.name ?? 'Аноним';
Enter fullscreen mode Exit fullscreen mode

Это очень удобный синтаксис. Монада 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);
Enter fullscreen mode Exit fullscreen mode

Разница лишь в том, что монадный подход делает обработку явной и универсальной, а цепочка может быть сколь угодно длинной и ветвистой

👀 Пример поинтереснее и более приближенный к реальности

👆 Развернуть
// Создаем переменные для повторного использования
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}`);
Enter fullscreen mode Exit fullscreen mode

Интересно, что современные фичи 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)
  );
Enter fullscreen mode Exit fullscreen mode

👀 Реализация и приближенный к реальности пример

👆 Развернуть
// Базовая имплементация 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}!`)
  );
Enter fullscreen mode Exit fullscreen mode

✅ Делает обработку ошибок явной и предсказуемой
✅ Улучшает читаемость кода при сложной логике

❌ Названия 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);
Enter fullscreen mode Exit fullscreen mode

Метод .then() здесь выполняет роль монадического chain. А .catch() работает как обработчик ошибочного пути из Either.

Главное отличие в том, что Promise работает асинхронно и автоматически оборачивает возвращаемые значения.


💡 Что мне дали монады в реальной работе?

Когда я наконец разобрался с монадами, то обнаружил, что они решают вполне практические задачи:

  • ✅ Упрощают сложные цепочки операций с проверками
  • ✅ Делают код более предсказуемым и безопасным
  • ✅ Снижают количество ветвлений и вложенных условий

На практике многие из нас уже используют монадоподобные подходы:

  • Цепочки .then() для промисов
  • Операторы ?. и ?? для безопасного доступа к свойствам

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

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

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


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

Top comments (0)

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay