- PVSM.RU - https://www.pvsm.ru -

Функциональное программирование (ФП) может улучшить ваш подход к написанию кода. Но ФП непросто освоить. Многие статьи и руководства не уделяют внимания таким подробностям, как монады (Monads), аппликативность (Applicative) и т. д., не приводят в качестве иллюстраций практические примеры, которые могли бы помочь нам в повседневном использовании мощных ФП-методик. Я решил исправить это упущение.
Хочу подчеркнуть: в статье сделан упор на том, ЗАЧЕМ нужна фича Х, а не на том, ЧТО такое фича Х.
ФП — это стиль написания программ, при котором просто комбинируется набор функций. В частности, ФП подразумевает обёртывание в функции практически всего подряд. Приходится писать много маленьких многократно используемых функций и вызывать их одну за другой, чтобы получить результат вроде (func1.func2.func3) или комбинации типа func1(func2(func3())).
Но чтобы действительно писать программы в таком стиле, функции должны следовать определённым правилам и решать некоторые проблемы.
Если всё можно сделать путём комбинирования набора функций, то…
Для решения всех этих проблем полностью функциональные языки вроде Haskell из коробки предоставляют разные инструменты и математические концепции, например монады, функторы и т. д. JavaScript из коробки не даёт такого обилия инструментов, но, к счастью, у него достаточный набор ФП-свойств, позволяющих писать библиотеки.
Если библиотеки хотят предоставить такие возможности, как функторы, монады и пр., то им нужно реализовать функции/классы, удовлетворяющие определённым спецификациям, чтобы предоставляемые возможности были такими же, как в языках вроде Haskell.
Яркий пример — спецификации Fantasy Land [1], объясняющие, как должна себя вести каждая JS-функция/класс.
На иллюстрации изображены все спецификации и их зависимости. Спецификации — по сути законы, они аналогичны интерфейсам в Java. С точки зрения JS спецификации можно рассматривать как классы или функции-конструкторы, реализующие в соответствии со спецификацией некоторые методы (вроде map, of, chain и т. д.).
Например:
JS-класс — это функтор (Functor), если он реализует метод map. И метод должен работать так, как предписано спецификацией (объяснение упрощённое, правил на самом деле больше).
JS-класс — это функтор Apply (Apply Functor), если он в соответствии со спецификацией реализует функции map и ap.
JS-класс — это монада (Monad Functor), если он реализует требования Functor, Apply, Applicative, Chain и самой Monad (в соответствии с цепочкой зависимостей).
Примечание: зависимость может выглядеть как наследование, но необязательно. Например, монада реализует обе спецификации — Applicative и Chain (в дополнение к остальным).
Есть несколько библиотек, реализующих спецификации FL. Например: monet.js [2], barely-functional [3], folktalejs [4], ramda-fantasy [5] (на базе Ramda), immutable-ext [6] (на базе ImmutableJS), Fluture [7] и др.
Библиотеки наподобие lodash-fp [8] и ramdajs [9] позволят только начать писать в стиле ФП. Но они не предоставляют функции для использования ключевых математических концепций вроде монад, функторов или редьюсера (Foldable), позволяющих решать реальные проблемы.
Так что я бы вдобавок порекомендовал выбрать одну из библиотек, использующих спецификации FL: monet.js [2], barely-functional [3], folktalejs [4], ramda-fantasy [5] (на базе Ramda), immutable-ext [6] (на базе ImmutableJS), Fluture [7] и т. д.
Примечание: я пользуюсь ramdajs [9] и ramda-fantasy [5].
Итак, мы получили представление об основах, теперь перейдём к практическим примерам и изучим различные возможности и методики ФП.
В разделе рассматриваются: функторы, монады, монады Maybe, каррирование
Применение: мы хотим показывать разные начальные страницы в зависимости от пользовательской настройки предпочтительного языка. Нужно написать getUrlForUser, возвращающий соответствующий URL из списка URL’ов (indexURL’ы) для пользовательского (joeUser) предпочтительного языка (испанский).

Проблема: язык не может быть null. И пользователь тоже не может быть null (не залогинен). Языка может не быть в нашем списке indexURL’ов. Поэтому нам нужно позаботиться о многочисленных nulls или undefined.
//TODO Напишите это в императивном и функциональном стилях
const getUrlForUser = (user) => {
//todo
}
//Пользовательский объект
let joeUser = {
name: 'joe',
email: 'joe@example.com',
prefs: {
languages: {
primary: 'sp',
secondary: 'en'
}
}
};
//Глобальная схема indexURL’ов для разных языков
let indexURLs = {
'en': 'http://mysite.com/en', //Английский
'sp': 'http://mysite.com/sp', //Испанский
'jp': 'http://mysite.com/jp' //Японский
}
//apply url to window.location
const showIndexPage = (url) => { window.location = url };
Не переживайте, если ФП-версия выглядит трудной для понимания. Дальше в этой статье мы разберём её шаг за шагом.
//Императивная версия:
//Слишком много if-else и проверок на null; зависимость от глобальных indexURL’ов; «английские» URL’ы берутся для всех стран по умолчанию
const getUrlForUser = (user) => {
if (user == null) { //не залоггирован
return indexURLs['en']; //возвращает страницу по умолчанию
}
if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') {
if (indexURLs[user.prefs.languages.primary]) {//если есть локализованная версия, то возвращает indexURLs[user.prefs.languages.primary];
} else {
return indexURLs['en'];
}
}
}
//вызов
showIndexPage(getUrlForUser(joeUser));
//Функциональное программирование:
//(Сначала сложно для понимания, но получается надёжнее, багов меньше)
//Используемые ФП-методики: функторы, монада Maybe и Currying
const R = require('ramda');
const prop = R.prop;
const path = R.path;
const curry = R.curry;
const Maybe = require('ramda-fantasy').Maybe;
const getURLForUser = (user) => {
return Maybe(user)//обёртываем пользователя в объект Maybe
.map(path(['prefs', 'languages', 'primary'])) //используем Ramda для получения первичного языка
.chain(maybeGetUrl); //передаём язык в maybeGetUrl и получаем URL или монаду null
}
const maybeGetUrl = R.curry(function(allUrls, language) {//преобразуем в функцию с одним аргументом
return Maybe(allUrls[language]);//возвращаем монаду (url | null)
})(indexURLs);// вместо глобального доступа передаём indexURLs
function boot(user, defaultURL) {
showIndexPage(getURLForUser(user).getOrElse(defaultURL));
}
boot(joeUser, 'http://site.com/en'); //'http://site.com/sp'
Давайте сначала разберём ФП-концепции и методики, использованные в этом решении.
Любой класс (или функция-конструктор) или тип данных, хранящий значение и реализующий метод map, называется функтором (Functor).
Например: массив — это функтор, потому что он может хранить значения и имеет метод map, позволяющий нам применить (map) функцию к хранимым значениям.
const add1 = (a) => a+1;
let myArray = new Array(1, 2, 3, 4); //хранимые значения
myArray.map(add1) // -> [2,3,4,5] //применение функций
Напишем собственный функтор — MyFunctor. Это просто JS-класс (функция-конструктор), который хранит какое-то значение и реализует метод map. Этот метод применяет функцию к хранимому значению, а затем из результата создаёт новый Myfunctor и возвращает его.
const add1 = (a) => a + 1;
class MyFunctor { //кастомный функтор
constructor(value) {
this.val = value;
}
map(fn) { //применяет функцию к this.val + возвращает новый Myfunctor
return new Myfunctor(fn(this.val));
}
}
//temp — это экземпляр функтора, хранящий значение 1
let temp = new MyFunctor(1);
temp.map(add1) //-> temp позволяет нам преобразовать (map) "add1"
P. S. Функторы должны реализовывать и другие спецификации (Fantasy-land [1]) в дополнение к map, но здесь мы этого касаться не будем.
Монады тоже функторы, т. е. у них есть метод map. Но они реализуют не только его. Если вы снова взглянете на схему зависимостей, то увидите, что монады должны реализовывать разные функции из разных спецификаций, например Apply [10] (метод ap), Applicative [11] (методы ap и of) и Chain [12] (метод chain).

Упрощённое объяснение. В JS монады — это классы или функции-конструкторы, хранящие какие-то данные и реализующие методы map, ap, of и chain, которые что-то делают с хранимыми данными в соответствии со спецификациями.
Вот образец реализации, чтобы вы понимали внутреннее устройство монад.
//Монада — образец реализации
class Monad {
constructor(val) {
this.__value = val;
}
static of(val) {//Monad.of проще, чем new Monad(val)
return new Monad(val);
};
map(f) {//Применяет функцию, но возвращает другую монаду!
return Monad.of(f(this.__value));
};
join() { // используется для получения значения из монады
return this.__value;
};
chain(f) {//вспомогательная функция, преобразующая (map), а затем извлекающая значение
return this.map(f).join();
};
ap(someOtherMonad) {//используется для работы с несколькими монадами
return someOtherMonad.map(this.__value);
}
}
Обычно используются монады не общего назначения, а более специфические и более полезные. Например, Maybe или Either.
Монада Maybe
Монада Maybe — это класс, реализующий спецификацию монады. Но особенность монады в том, что она корректно обрабатывает значения null или undefined.
В частности, если хранимые данные являются null или undefined, то функция map вообще не выполняет данную функцию, потому не возникает проблем с null и undefined. Такая монада используется в ситуациях, когда приходится иметь дело с null-значениями.
В коде ниже представлена ramda-fantasy реализация монады Maybe. В зависимости от значения она создаёт экземпляр одного из двух разных подклассов — Just или Nothing (значение или полезное, или null/undefined).
Хотя методы Just и Nothing одинаковы (map, orElse и т. д.), Just что-то делает, а Nothing не делает ничего.
Обратите особое внимание на методы map и orElse в этом коде:
//Здесь приведены фрагменты реализации Maybe из библиотеки ramda-fantasy
//Полный код доступен по ссылке: https://github.com/ramda/ramda-fantasy/blob/master/src/Maybe.js
function Maybe(x) { //<-- главный конструктор, возвращающий монаду Maybe подкласса Just или Nothing
return x == null ? _nothing : Maybe.Just(x);
}
function Just(x) {
this.value = x;
}
util.extend(Just, Maybe);
Just.prototype.isJust = true;
Just.prototype.isNothing = false;
function Nothing() {}
util.extend(Nothing, Maybe);
Nothing.prototype.isNothing = true;
Nothing.prototype.isJust = false;
var _nothing = new Nothing();
Maybe.Nothing = function() {
return _nothing;
};
Maybe.Just = function(x) {
return new Just(x);
};
Maybe.of = Maybe.Just;
Maybe.prototype.of = Maybe.Just;
// функтор
Just.prototype.map = function(f) { //Делает map, когда Just выполняет функцию, и возвращает Just на основании результата
return this.of(f(this.value));
};
Nothing.prototype.map = util.returnThis; // <-- Делает map, когда Nothing ничего не делает
Just.prototype.getOrElse = function() {
return this.value;
};
Nothing.prototype.getOrElse = function(a) {
return a;
};
module.exports = Maybe;
Давайте посмотрим, как можно использовать монаду Maybe для работы с проверками на null.
Пойдём поэтапно:
//Этап 1. Вместо...
if (user == null) { //не залоггирован
return indexURLs['en']; //возвращаем страницу по умолчанию
}
//Используем:
Maybe(user) //Возвращает Maybe({userObj}) или Maybe(null). То есть данные завёрнуты ВНУТРИ Maybe
//Этап 2. Вместо...
if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') {
if (indexURLs[user.prefs.languages.primary]) {//если существует локализованная страница,
return indexURLs[user.prefs.languages.primary];
//Используем:
//библиотеку, умеющую работать с данными внутри Maybe, вроде map.path из Ramda:
<userMaybe>.map(path(['prefs', 'languages', 'primary']))
//Этап 3. Вместо...
return indexURLs['en']; //hardcoded default values
//Используем:
//все Maybe-библиотеки предоставляют метод orElse или getOrElse, который вернёт либо актуальные данные, либо значение по умолчанию
<userMayBe>.getOrElse('http://site.com/en')
В разделе рассматриваются: чистые функции (Pure functions) и композиция (Composition)
Если мы хотим составить цепочки функций — func1.func2.func3 или (func1(func2(func3())), то каждая из них может брать лишь по одному параметру. Например, если func2 берёт два параметра — func2(param1, param2), то мы не сможем включить её в цепочку!
Но ведь на практике многие функции берут по несколько параметров. Так как же нам их комбинировать? Решение: каррирование (Currying).
Каррирование — это преобразование функции, берущей несколько параметров за один раз, в функцию, берущую только один параметр. Функция не выполнится, пока не будут переданы все параметры.
Кроме того, каррирование можно использовать при обращении к глобальным переменным, т. е. делать это «чисто».
Посмотрим снова на наше решение:
//Ниже показано, как работать с глобальными переменными и при этом делать функции пригодными для включения в цепочки
//Глобал indexURLs сопоставлен с разными языками
let indexURLs = {
'en': 'http://mysite.com/en', //English
'sp': 'http://mysite.com/sp', //Spanish
'jp': 'http://mysite.com/jp' //Japanese
}
//Императивное решение
const getUrl = (language) => allUrls[language]; //Просто, но грязно (обращение к глобальной переменной), и есть риск ошибок
//Функциональное решение
//До каррирования:
const getUrl = (allUrls, language) => {
return Maybe(allUrls[language]);
}
//После каррирования:
const getUrl = R.curry(function(allUrls, language) {//curry преобразует это в функцию с одним аргументом
return Maybe(allUrls[language]);
});
const maybeGetUrl = getUrl(indexURLs) //Хранит в функции curried глобальное значение.
//С этого момента maybeGetUrl нужен только один аргумент (язык). Можно сделать цепочку наподобие
maybe(user).chain(maybeGetUrl).bla.bla
В разделе рассматривается: монада Either
Монада Maybe очень удобна, если у нас есть значения «по умолчанию» для замены Null-ошибок. Но что насчёт функций, которым действительно нужно кидать ошибки? И как узнать, какая функция кинула ошибку, если мы собрали в цепочку несколько кидающих ошибки функций? То есть нам нужен быстрый отказ (fast-failure).
Например: у нас есть цепочка func1.func2.func3…, и если func2 кинула ошибку, то нужно пропустить func3 и последующие функции и правильно показать ошибку из func2 для дальнейшей обработки.
Монады Either отлично подходят для работы с несколькими функциями, когда любая из них может кинуть ошибку и захотеть немедленно после этого выйти, так что мы можем определить, где именно произошла ошибка.
Применение: в приведённом ниже императивном коде мы вычисляем tax и discount для item’ов и отображаем showTotalPrice.
Обратите внимание, что функция tax кинет ошибку, если значение цены будет нечисловое. По той же причине кинет ошибку и функция discount. Но, кроме того, discount кинет ошибку, если цена item’а меньше 10.
Поэтому в showTotalPrice проводятся проверки на наличие ошибок.
//Императивное решение
//Возвращает либо ошибку, либо цену с учётом налога
const tax = (tax, price) => {
if (!_.isNumber(price)) return new Error("Price must be numeric");
return price + (tax * price);
};
// Возвращает либо ошибку, либо цену с учётом скидки
const discount = (dis, price) => {
if (!_.isNumber(price)) return (new Error("Price must be numeric"));
if (price < 10) return new Error("discount cant be applied for items priced below 10");
return price - (price * dis);
};
const isError = (e) => e && e.name == 'Error';
const getItemPrice = (item) => item.price;
//Показывает общую цену с учётом налога и скидки. Должен обрабатывать ошибки.
const showTotalPrice = (item, taxPerc, disount) => {
let price = getItemPrice(item);
let result = tax(taxPerc, price);
if (isError(result)) {
return console.log('Error: ' + result.message);
}
result = discount(discount, result);
if (isError(result)) {
return console.log('Error: ' + result.message);
}
//показывает результат
console.log('Total Price: ' + result);
}
let tShirt = { name: 't-shirt', price: 11 };
let pant = { name: 't-shirt', price: '10 dollars' };
let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error
showTotalPrice(tShirt) // Сумма: 9,075
showTotalPrice(pant) // Ошибка: цена должна быть числом
showTotalPrice(chips) // Ошибка: скидка не применяется к цене ниже 10
Посмотрим, как можно улучшить showTotalPrice с помощью монады Either и переписать всё в ФП-стиле.
Монада Either предоставляет два конструктора: Either.Left и Either.Right. Их можно считать подклассами Either. Left и Right — это монады! Идея в том, чтобы хранить ошибки/исключения в Left, а полезные значения — в Right. То есть в зависимости от значения создаём экземпляр Either.Left или Either.Right. Сделав так, мы можем применить к этим значениям map, chain и т. д.
Хотя и Left, и Right предоставляют map, chain и пр., конструктор Left только хранит ошибки, а все функции реализует конструктор Right, потому что он хранит фактический результат.
Теперь посмотрим, как можно преобразовать наш императивный код в функциональный.
Этап 1. Обернём возвращаемые значения в Left и Right.
Примечание: «обернём» означает «создадим экземпляр какого-то класса». Эти функции внутри себя вызывают new, так что нам не придётся это делать.
var Either = require('ramda-fantasy').Either;
var Left = Either.Left;
var Right = Either.Right;
const tax = R.curry((tax, price) => {
if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<--Обернём ошибку в Either.Left
return Right(price + (tax * price)); //<--Обернём результат в Either.Right
});
const discount = R.curry((dis, price) => {
if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<--Wrap Error in Either.Left
if (price < 10) return Left(new Error("discount cant be applied for items priced below 10")); //<-- Обернём ошибку в Either.Left
return Right(price - (price * dis)); //<--Обернём результат в Either.Right
});
Этап 2. Обернём исходное значение в Right, потому что оно валидное и мы можем его комбинировать (compose).
const getItemPrice = (item) => Right(item.price);
Этап 3. Создадим две функции: одну для обработки ошибки, вторую для обработки результата. Обернём их в Either.either (из ramda-fantasy.js api [13]).
Either.either берёт три параметра: обработчика результата, обработчика ошибки и монаду Either. Either каррирована, поэтому мы можем передать обработчики сейчас, а Either (третий параметр) — позже.
Как только Either.either получает все три параметра, она передаёт Either либо в обработчик результата, либо в обработчик ошибки, в зависимости от того, чем является Either — Right или Left.
const displayTotal = (total) => { console.log(‘Total Price: ‘ + total) };
const logError = (error) => { console.log(‘Error: ‘ + error.message); };
const eitherLogOrShow = Either.either(logError, displayTotal);
Этап 4. Используем метод chain для комбинирования функций, кидающих ошибки. Передадим их результаты в Either.either (eitherLogOrShow), которая позаботится о том, чтобы ретранслировать их в обработчик результата или ошибки.
const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax));
Соберём всё вместе:
const tax = R.curry((tax, price) => {
if (!_.isNumber(price)) return Left(new Error("Price must be numeric"));
return Right(price + (tax * price));
});
const discount = R.curry((dis, price) => {
if (!_.isNumber(price)) return Left(new Error("Price must be numeric"));
if (price < 10) return Left(new Error("discount cant be applied for items priced below 10"));
return Right(price - (price * dis));
});
const addCaliTax = (tax(0.1));//налог 10 %
const apply25PercDisc = (discount(0.25));// скидка 25 %
const getItemPrice = (item) => Right(item.price);
const displayTotal = (total) => { console.log('Total Price: ' + total) };
const logError = (error) => { console.log('Error: ' + error.message); };
const eitherLogOrShow = Either.either(logError, displayTotal);
//api
const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax));
let tShirt = { name: 't-shirt', price: 11 };
let pant = { name: 't-shirt', price: '10 dollars' }; //error
let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error
showTotalPrice(tShirt) // Сумма: 9,075
showTotalPrice(pant) // Ошибка: цена должна быть числом
showTotalPrice(chips) // Ошибка: скидка не применяется к цене ниже 10
***Использована ФП-концепция: аппликативность (Applicative)
Применение: допустим, нужно дать пользователю скидку, если он залогинился и если действует промоакция (т. е. существует скидка).

Воспользуемся методом applyDiscount. Он может кидать null-ошибки, если пользователь (слева) или скидка (справа) является null.
//Добавим скидку в объект user, если существует и пользователь, и скидка.
//Null-ошибки кидаются в том случае, если пользователь или скидка является null.
const applyDiscount = (user, discount) => {
let userClone = clone(user);// С помощью какой-нибудь библиотеки сделаем копию
userClone.discount = discount.code;
return userClone;
}
Посмотрим, как можно решить это с помощью аппликативности.
Аппликативность (Applicative)
Любой класс, имеющий метод ap и реализующий спецификацию Applicative, называется аппликативным. Такие классы могут использоваться в функциях, которые работают с null-значениями как с левой стороны (пользователь) уравнения, так и с правой (скидка).
Монады Maybe (как и все монады) тоже реализуют спецификацию ap, а значит, они тоже аппликативные, а не просто монады. Поэтому на функциональном уровне мы можем использовать монады Maybe для работы с null. Посмотрим, как заставить работать applyDiscount с помощью монады Maybe, используемой как аппликативная.
Этап 1. Обернём потенциальные null-значения в монады Maybe.
const maybeUser = Maybe(user);
const maybeDiscount = Maybe(discount);
Этап 2. Перепишем функцию и каррируем её, чтобы передавать по одному параметру за раз.
// перепишем функцию и каррируем её, чтобы
// передавать по одному параметру за раз
var applyDiscount = curry(function(user, discount) {
user.discount = discount.code;
return user;
});
Этап 3. Передадим через map первый аргумент (maybeUser) в applyDiscount.
// передадим через map первый аргумент (maybeUser) в applyDiscount
const maybeApplyDiscountFunc = maybeUser.map(applyDiscount);
//Обратите внимание, что поскольку applyDiscount каррирована и map передаст только один параметр, то возвращаемый результат (maybeApplyDiscountFunc) будет обёрнутой в монаду Maybe функцией applyDiscount, которая в своём замыкании теперь содержит maybeUser(первый параметр).
Иными словами, теперь у нас есть функция, обёрнутая в монаду!
Этап 4. Работаем с maybeApplyDiscountFunc.
На этом этапе maybeApplyDiscountFunc может быть:
1) функцией, обёрнутой в Maybe, — если пользователь существует;
2) Nothing (подклассом Maybe) — если пользователь не существует.
Если пользователь не существует, то возвращается Nothing, а все последующие действия с ним полностью игнорируются. Так что если мы передадим второй аргумент, то ничего не произойдёт. Также не будет null-ошибок.
Если пользователь существует, то можем попытаться передать второй аргумент в maybeApplyDiscountFunc через map, чтобы выполнить функцию:
maybeDiscount.map(maybeApplyDiscountFunc)! // ПРОБЛЕМА!
Ой-ёй: map не знает, как выполнять функцию (maybeApplyDiscountFunc), когда она сама внутри Maybe!
Поэтому нам нужен другой интерфейс для работы по такому сценарию. И этот интерфейс — ap!
Этап 5. Освежим информацию о функции ap. Метод ap берёт другую монаду Maybe и передаёт/применяет к ней хранимую им в данный момент функцию.
class Maybe {
constructor(val) {
this.val = val;
}
...
...
//ap берёт другую maybe и применяет к ней хранящуюся в нём функцию.
//this.val ДОЛЖЕН быть функцией или Nothing (и не может быть строкой или числом)
ap(differentMayBe) {
return differentMayBe.map(this.val);
}
}
Можем просто применить (ap) maybeApplyDiscountFunc к maybeDiscount вместо использования map, как показано ниже. И это будет прекрасно работать!
maybeApplyDiscountFunc.ap(maybeDiscount)
//Поскольку applyDiscount хранится внутри this.val в обёртке maybeApplyDiscountFunc:
maybeDiscount.map(applyDiscount)
//Далее, если maybeDiscount имеет скидку, то функция выполняется. Если maybeDiscount является Null, то ничего не происходит.
Для сведения: очевидно, в спецификации Fantasy Land внесли изменение. В старой версии нужно было писать: Just(f).ap(Just(x)), где f — это функция, х — значение. В новой версии нужно писать Just(x).ap(Just(f)). Но реализации по большей части пока не изменились. Спасибо keithalexander [14].
Подведём итог. Если у вас есть функция, работающая с несколькими параметрами, каждый из которых может быть null, то сначала каррируйте её, а затем поместите внутрь Maybe. Также поместите в Maybe все параметры, а для исполнения функции воспользуйтесь ap.
Мы уже знакомы с каррированием. Это простое преобразование функции, чтобы она брала не несколько аргументов сразу, а по одному.
//Пример каррирования:
const add = (a, b) =>a+b;
const curriedAdd = R.curry(add);
const add10 = curriedAdd(10);//Передаёт первый аргумент. Возвращает функцию, берущую второй (b) параметр.
//Запускает функцию после передачи второго параметра.
add10(2) // -> 12 //внутренне запускает add с 10 и 2.
Но что если вместо добавления всего двух чисел функция add могла суммировать все числа, передаваемые в неё в качестве аргументов?
const add = (...args) => R.sum(args); //Суммирует все числа в аргументах
Мы всё ещё можем каррировать её, ограничив количество аргументов с помощью curryN:
//пример curryN
const add = (...args) => R.sum(args);
//пример CurryN:
const add = (...args) => R.sum(args);
const add3Numbers = R.curryN(3, add);
const add5Numbers = R.curryN(5, add);
const add10Numbers = R.curryN(10, add);
add3Numbers(1,2,3) // 6
add3Numbers(1) // возвращает функцию, берущую ещё два параметра.
add3Numbers(1, 2) // возвращает функцию, берущую ещё один параметр.
Допустим, нам нужна функция, которая пишет в лог только тогда, когда мы вызвали её три раза (один и два вызова игнорируются). Например:
//грязное решение
let counter = 0;
const logAfter3Calls = () => {
if(++counter == 3)
console.log('called me 3 times');
}
logAfter3Calls() // Ничего не происходит
logAfter3Calls() // Ничего не происходит
logAfter3Calls() // 'called me 3 times'
Можем эмулировать такое поведение с помощью curryN.
//чистое решение
const log = () => {
console.log('called me 3 times');
}
const logAfter3Calls = R.curryN(3, log);
//вызов
logAfter3Calls('')('')('')//'called me 3 times'
//Примечание: мы передаём '', чтобы удовлетворить CurryN фактом передачи параметра.
Примечание: мы будем использовать эту методику при аппликативной валидации.
В разделе рассматривается: валидация (известна как функтор Validation, аппликативность Validation, монада Validation).
Валидациями обычно называют аппликативность Validation (Validation Applicative), потому что она чаще всего применяется для валидации с использованием функции ap (apply).
Валидации аналогичны монадам Either и часто используются для работы с комбинациями функций, кидающих ошибки. Но в отличие от Either, в которых мы для комбинирования обычно применяем метод chain, в монадах Validation мы для этого обычно используем метод ap. Кроме того, если chain позволяет собирать только первую ошибку, то ap, особенно в монадах Validation, даёт собирать в массив все ошибки.
Обычно эти монады используются при валидации форм ввода, когда нам нужно сразу показать все ошибки.
Применение: у нас есть форма регистрации, которая проверяет имя, пароль и почту с помощью трёх функций (isUsernameValid, isPwdLengthCorrect и ieEmailValid). Нам нужно одновременно показать все три ошибки, если они возникнут.

Для этого воспользуемся функтором Validation. Посмотрим, как это можно реализовать с помощью аппликативности Validation.
Возьмём из folktalejs [15] библиотеку data.validation, в ramda-fantasy она ещё не реализована. По аналогии с монадой Either у нас есть два конструктора: Success и Failure. Это подклассы, каждый из которых реализует спецификации Either.
Этап 1. Для использования валидации нам нужно обернуть правильные значения и ошибки в конструкторы Success и Failure (т. е. создать экземпляры этих классов).
const Validation = require('data.validation') //из folktalejs
const Success = Validation.Success
const Failure = Validation.Failure
const R = require('ramda');
//вместо:
function isUsernameValid(a) {
return /^(0|[1-9][0-9]*)$/.test(a) ?
["Username can't be a number"] : a
}
//используем:
function isUsernameValid(a) {
return /^(0|[1-9][0-9]*)$/.test(a) ?
Failure(["Username can't be a number"]) : Success(a)
}
Повторите процесс для всех проверочных функций, кидающих ошибки.
Этап 2. Создадим пустую функцию (dummy function) для хранения успешного статуса проверки.
const returnSuccess = () => 'success';//просто возвращает success
Этап 3. Используем curryN для многократного применения ap
С ap есть проблема: левая часть должна быть функтором (или монадой), содержащим функцию.
Допустим, нам нужно многократно применить ap. Это будет работать, только если monad1 содержит функцию. И результат monad1.ap(monad2), т. е. resultingMonad, тоже должен быть монадой с функцией, чтобы можно было применить ap к monad3.
let finalResult = monad1.ap(monad2).ap(monad3)
//можно переписать как
let resultingMonad = monad1.ap(monad2)
let finalResult = resultingMonad.ap(monad3)
//будет работать, если monad1 содержит функцию, а результат monad1.ap(monad2) является другой монадой (resultingMonad) с функцией
В общем, чтобы дважды применить ap, нам нужны две монады, содержащие функции.
В данном случае у нас три функции, к которым нужно применить ap.
Допустим, мы сделали что-то такое.
Success(returnSuccess)
.ap(isUsernameValid(username)) //работает
.ap(isPwdLengthCorrect(pwd))//не работает
.ap(ieEmailValid(email))//не работает
Предыдущий код не станет работать, потому что результатом Success(returnSuccess).ap(isUsernameValid(username)) будет значение. И мы не сможем применить ap ко второй и третьей функциям.
Можно использовать curryN, чтобы возвращать функцию до тех пор, пока она не будет вызвана N раз.
//3 — потому что вызываем ap три раза.
let success = R.curryN(3, returnSuccess);
Теперь каррированная success возвращает функцию три раза.
function validateForm(username, pwd, email) {
//3 — потому что вызываем ap три раза.
let success = R.curryN(3, returnSuccess);
return Success(success)// по умолчанию; используется для трёх ap
.ap(isUsernameValid(username))
.ap(isPwdLengthCorrect(pwd))
.ap(ieEmailValid(email))
}
Соберём всё вместе:
const Validation = require('data.validation') //из folktalejs
const Success = Validation.Success
const Failure = Validation.Failure
const R = require('ramda');
function isUsernameValid(a) {
return /^(0|[1-9][0-9]*)$/.test(a) ? Failure(["Username can't be a number"]) : Success(a)
}
function isPwdLengthCorrect(a) {
return a.length == 10 ? Success(a) : Failure(["Password must be 10 characters"])
}
function ieEmailValid(a) {
var re = /^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;
return re.test(a) ? Success(a) : Failure(["Email is not valid"])
}
const returnSuccess = () => 'success';//просто возвращает success
function validateForm(username, pwd, email) {
let success = R.curryN(3, returnSuccess);// 3 — потому что вызываем ap три раза.
return Success(success)
.ap(isUsernameValid(username))
.ap(isPwdLengthCorrect(pwd))
.ap(ieEmailValid(email))
}
validateForm('raja', 'pwd1234567890', 'r@r.com').value;
//Output: success
validateForm('raja', 'pwd', 'r@r.com').value;
//Output: ['Password must be 10 characters' ]
validateForm('raja', 'pwd', 'notAnEmail').value;
//Output: ['Password must be 10 characters', 'Email is not valid']
validateForm('123', 'pwd', 'notAnEmail').value;
//['Username can't be a number', 'Password must be 10 characters', 'Email is not valid']
Автор: Mail.Ru Group
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/253980
Ссылки в тексте:
[1] спецификации Fantasy Land: https://github.com/fantasyland/fantasy-land
[2] monet.js: https://cwmyers.github.io/monet.js/
[3] barely-functional: https://github.com/cullophid/barely-functional
[4] folktalejs: http://folktalejs.org/
[5] ramda-fantasy: https://github.com/ramda/ramda-fantasy
[6] immutable-ext: https://github.com/DrBoolean/immutable-ext
[7] Fluture: https://github.com/Avaq/Fluture
[8] lodash-fp: https://github.com/lodash/lodash/wiki/FP-Guide
[9] ramdajs: http://ramdajs.com/
[10] Apply: https://github.com/fantasyland/fantasy-land#apply
[11] Applicative: https://github.com/fantasyland/fantasy-land#applicative
[12] Chain: https://github.com/fantasyland/fantasy-land#chain
[13] ramda-fantasy.js api: https://github.com/ramda/ramda-fantasy/blob/master/src/Either.js#L33
[14] keithalexander: https://medium.com/@keithalexander
[15] folktalejs: https://github.com/folktale/data.validation
[16] Источник: https://habrahabr.ru/post/327522/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.