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 в JavaScriptPromise.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-обращений часты.