Functors, Monads и Applicatives

Функтор — обёртка над значением с методом map. Аппликативный функтор добавляет ap (применение функции, лежащей в другом функторе). Монада добавляет chain/flatMap для построения цепочек контейнеров.

Функтор: контейнер + map

Функтор — это объект, который хранит в себе какую-то величину и через который мы можем эту величину преобразовывать.

class Maybe {
  constructor(value) { this._value = value; }
  static of(value) { return new Maybe(value); }
  isNothing { return this._value == null; }
  map(fn) { return this.isNothing ? this : Maybe.of(fn(this._value)); }
}

Maybe.of(5)
  .map(x => x * 2)
  .map(x => x + 1);

Так как мы храним значение в объекте, объект это аналог замыкания, только для OOP. Мы практически в коробочке храним значение и можем преобразовывать его через функции.

Maybe на замыканиях — функтор без классов

const maybe = (x) => (fn) => maybe((x && fn) ? fn(x) : null);

maybe(5)(x => x * 2)(x => x + 1)(console.log); // 11

Возврат вызова maybe из замыкания = рекурсивное замыкание. Без new, чистый функциональный chaining.

Аппликативный функтор: ap

class Functor {
  constructor(value) { this._value = value; }
  static of(value) { return new Functor(value); }
  map(fn) { return Functor.of(fn(this._value)); }
  ap(functorFn) {
    return functorFn.map(fn => fn(this._value));
  }
}

const addOne = Functor.of(x => x + 1);
Functor.of(5).ap(addOne); // Functor(6)

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

В JS используем имя ap, потому что apply занят на Function.prototype.

Монада: chain/flatMap

Map для отображения через другую функцию, ap для применения функции в другом функторе, и chain для построения цепочек. Функтор с ap и chain называется монадой.

class Maybe {
  // ...
  chain(fn) {
    return this.isNothing ? this : fn(this._value);
    // fn возвращает уже Maybe — не оборачиваем повторно
  }
}

Без chain последовательное применение функций, возвращающих Maybe, давало бы Maybe<Maybe<Maybe<T>>>. chain "схлопывает" вложенные обёртки.

Промис как монада

Метод Аналог
Promise.resolve of
.then(fn) где fn → значение map
.then(fn) где fn → Promise chain/flatMap

Promise не следует всем законам монад строго, но работает как монада на практике.

Зачем — глубокий доступ к конфигу

// Без функтора — лесенка проверок
const fname = config && config.server && config.server.ssl
  && config.server.ssl.key && config.server.ssl.key.filename;

// С Maybe / path-функтором
const fname = Maybe.of(config)
  .map(c => c.server)
  .map(s => s.ssl)
  .map(k => k.filename)
  .getOrElse(null);

Одна строка, написанная при помощи функторов, эквивалентна вот такой вот лестнице if-ов. В JS функторы нужны для улучшения синтаксиса.

path через reduce

const path = (str, data) =>
  str.split('.').reduce((acc, key) => acc?.[key], data);

path('server.ssl.key.filename', config);

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

Ресурсы