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);
Связанные темы
- Functor и Applicative
- Monad Pattern -- основы
- Контейнерные типы Result Maybe Either
- Композиция функций (pipe, compose)
Ресурсы
- Лекция: 3Z7f0Gi8pxw
- Лекция Замыкания, обёртки, функторы: 8_aKQBhzpl8