Learning Book

Maybe и Either монады

Зачем монады — без теории

Монады — это просто способ обернуть значение и управлять тем, как применяются функции. Никакой теории категорий — только практика.

Maybe — решает проблему null/undefined. Either — решает проблему обработки ошибок функционально.

Maybe — безопасная работа с null

class Maybe {
  constructor(value) {
    this._value = value
  }

  // Обёртка над значением
  static of(value) {
    return new Maybe(value)
  }

  // Применить функцию, только если значение не null/undefined
  map(fn) {
    if (this._value == null) return this  // null → пропускаем
    return Maybe.of(fn(this._value))
  }

  // Получить значение или default
  getOrElse(defaultValue) {
    return this._value ?? defaultValue
  }
}

// Использование
const user = { profile: { address: { city: 'Москва' } } }

// Без Maybe: нужны проверки на каждом уровне
const city1 = user?.profile?.address?.city ?? 'Неизвестно'

// С Maybe: цепочка без if/undefined
const city2 = Maybe.of(user)
  .map(u => u.profile)
  .map(p => p.address)
  .map(a => a.city)
  .getOrElse('Неизвестно')

// city1 === city2 === 'Москва'

// Если что-то null — не падаем
const noCity = Maybe.of(null)
  .map(u => u.profile)  // null → пропускается
  .map(p => p.city)     // пропускается
  .getOrElse('Нет города') // 'Нет города'
В современном JavaScript ?. (optional chaining) и ?? (nullish coalescing) — встроенный Maybe в синтаксисе языка. Maybe-монада полезна при построении более сложных пайплайнов трансформаций.

Either — функциональная обработка ошибок

Either имеет два состояния: Right (успех) и Left (ошибка). Функции применяются только к Right.

class Either {
  static right(value) {
    return { type: 'right', value, isRight: true }
  }

  static left(error) {
    return { type: 'left', error, isRight: false }
  }
}

function map(fn, either) {
  if (!either.isRight) return either  // ошибка — пропускаем
  try {
    return Either.right(fn(either.value))
  } catch (e) {
    return Either.left(e)
  }
}

// Использование в пайплайне
function parseUser(json) {
  try {
    return Either.right(JSON.parse(json))
  } catch (e) {
    return Either.left(new Error('Неверный JSON'))
  }
}

function validateUser(user) {
  if (!user.name) return Either.left(new Error('Имя обязательно'))
  return Either.right(user)
}

const result = map(
  validateUser,
  parseUser('{"name":"Алиса","age":25}')
)

if (result.isRight) {
  console.log('Пользователь:', result.value)
} else {
  console.error('Ошибка:', result.error.message)
}

Реальный пример: пайплайн обработки данных

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)

// Функции, возвращающие Either
const parseJSON = str => {
  try { return Either.right(JSON.parse(str)) }
  catch (e) { return Either.left(e) }
}

const getField = field => obj =>
  obj[field] != null
    ? Either.right(obj[field])
    : Either.left(new Error(`Поле ${field} отсутствует`))

// Безопасный map для Either
const mapRight = fn => either =>
  either.isRight ? map(fn, either) : either

// Пайплайн
const processInput = str => {
  let result = parseJSON(str)
  if (!result.isRight) return result

  const user = result.value
  if (!user.email) return Either.left(new Error('email обязателен'))

  return Either.right({
    email: user.email.toLowerCase(),
    name: user.name || 'Аноним'
  })
}

const r = processInput('{"email":"Test@Example.COM","name":"Алиса"}')
// Either.right({ email: "test@example.com", name: "Алиса" })