Functor и Applicative

Functor — структура данных с методом map, позволяющим применять функцию к значению внутри контейнера, не извлекая его; Applicative — Functor с методом ap для применения обёрнутой функции к обёрнутому значению.

Зачем нужно

Functor и Applicative — концепции функционального программирования, описывающие паттерн «значение в контейнере». В JavaScript Array.map — Functor над массивом; Promise.then — Functor над асинхронным значением; Maybe/Option — Functor для безопасной работы с null. Понимание этих абстракций помогает писать более composable код и понять библиотеки fp-ts, folktale, sanctuary.

Где используется

  • Array.map — встроенный Functor в JavaScript
  • Promise.then — Functor для асинхронных значений
  • Maybe / Option — Functor для nullable значений без if-проверок
  • Either / Result — Functor для обработки ошибок без try/catch
  • Библиотеки fp-ts, Ramda, folktale — типизированные Functors

Основной контент

Functor — контейнер с map

// Закон Functor:
// 1. Identity: F.map(x => x) === F (map с id не меняет структуру)
// 2. Composition: F.map(g(f)) === F.map(f).map(g)

// Array — встроенный Functor
const nums = [1, 2, 3];
const doubled = nums.map(x => x * 2); // [2, 4, 6]
// Значение трансформировано, контейнер (Array) сохранён

// Promise — Functor для асинхронных значений
const asyncVal = Promise.resolve(42);
asyncVal.then(x => x + 1); // Promise<43>

// Простой Functor — Box
class Box {
  constructor(value) {
    this.value = value;
  }

  map(fn) {
    return new Box(fn(this.value)); // применяем функцию, оборачиваем обратно
  }

  toString {
    return `Box(${this.value})`;
  }
}

const box = new Box(5);
console.log(box.map(x => x * 2).map(x => x + 1).toString()); // Box(11)

// Сравните с прямым вычислением:
// (5 * 2) + 1 = 11 — но через контейнер можно добавлять поведение (логирование, null-check)

Maybe — Functor для nullable значений

// Maybe решает проблему null/undefined без if-проверок в каждом вызове
class Maybe {
  constructor(value) {
    this.value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  isNothing {
    return this.value === null || this.value === undefined;
  }

  map(fn) {
    // Если Nothing — не применяем функцию, просто возвращаем Nothing
    return this.isNothing ? this : Maybe.of(fn(this.value));
  }

  getOrElse(defaultValue) {
    return this.isNothing ? defaultValue : this.value;
  }
}

// Без Maybe — бесконечные null-проверки
function getCity(user) {
  if (!user) return 'Unknown';
  if (!user.address) return 'Unknown';
  if (!user.address.city) return 'Unknown';
  return user.address.city;
}

// С Maybe — цепочка map без if
function getCityMaybe(user) {
  return Maybe.of(user)
    .map(u => u.address)
    .map(a => a.city)
    .getOrElse('Unknown');
}

console.log(getCityMaybe(null));                              // 'Unknown'
console.log(getCityMaybe({ address: null }));                 // 'Unknown'
console.log(getCityMaybe({ address: { city: 'Москва' } })); // 'Москва'

Applicative — применение обёрнутой функции

// Applicative добавляет метод ap (apply):
// ap позволяет применить Maybe(fn) к Maybe(value)

class MaybeApplicative extends Maybe {
  ap(maybeValue) {
    // this содержит функцию, maybeValue — значение
    if (this.isNothing || maybeValue.isNothing) {
      return MaybeApplicative.of(null);
    }
    return MaybeApplicative.of(this.value(maybeValue.value));
  }
}

// Применение обёрнутой функции к обёрнутому значению
const addMaybe = MaybeApplicative.of(x => y => x + y);
const num1 = MaybeApplicative.of(3);
const num2 = MaybeApplicative.of(4);

const result = addMaybe.ap(num1).ap(num2);
console.log(result.getOrElse(0)); // 7

// Если любое значение Nothing — результат Nothing
const nothing = MaybeApplicative.of(null);
const failResult = addMaybe.ap(nothing).ap(num2);
console.log(failResult.getOrElse(0)); // 0

// Практический пример: валидация формы через Applicative
// (несколько валидаторов применяются независимо, ошибки собираются)

Either — Functor для обработки ошибок

// Either: Right(value) — успех, Left(error) — ошибка
class Right {
  constructor(value) { this.value = value; }
  map(fn) { return new Right(fn(this.value)); }
  getOrElse(_) { return this.value; }
  fold(_, onRight) { return onRight(this.value); }
}

class Left {
  constructor(error) { this.error = error; }
  map(_) { return this; } // ошибка распространяется, fn не применяется
  getOrElse(defaultValue) { return defaultValue; }
  fold(onLeft, _) { return onLeft(this.error); }
}

function divide(a, b) {
  return b === 0 ? new Left('Деление на ноль') : new Right(a / b);
}

divide(10, 2)
  .map(x => x * 100)
  .fold(
    err => console.error('Ошибка:', err),
    val => console.log('Результат:', val) // 500
  );

divide(10, 0)
  .map(x => x * 100) // не выполняется
  .fold(
    err => console.error('Ошибка:', err), // 'Деление на ноль'
    val => console.log('Результат:', val)
  );

Частые ошибки

  • Functor нарушает закон identity: если map(x => x) меняет структуру (добавляет/удаляет элементы), это не валидный Functor.
  • Путаница с Monad: Monad добавляет к Functor метод chain/flatMap для избежания вложенности Box(Box(value)). Promise.then — одновременно и Functor и Monad.
  • Maybe/Either в простых случаях: если код и так читается без null-проверок, Maybe добавит лишнюю сложность. Применяйте там, где цепочки nullable-обращений часты.

Связанные темы

Ресурсы