В прошлой части мы рассмотрели основные инструменты функционального программирования с примерами на JavaScript: монады, функторы, каррирование. В этой статье мы закончим обзор принципов и инструментов, которые помогут вам построить поистине чистое приложение в функциональном стиле.
Пример 3. Задание значений объектам, которые могут равняться Null
Используемые концепты ФП: Аппликативные функторы.
Сценарий использования: Предположим, что мы хотим предоставить скидку пользователю, если пользователь залогинен и у нас есть действующее предложение (т.е. существует скидка).
Для этой задачи мы будем использовать метод applyDiscount, представленный ниже. Этот метод может выбрасывать ошибки типа null в случае, если пользователь или скидка равняется null.
// Предлагает пользователю скидку, если и пользователь, и скидка существуют
// Выбрасывает ошибку, если пользователь или скидка равны null
const applyDiscount = (user, discount) => {
let userClone = clone(user); // используем какую-нибудь библиотеку, чтобы создать копию объекта
userClone.discount = discount.code;
return userClone;
}
Давайте посмотрим, как мы можем решить эту задачу, используя аппликативные функторы.
Аппликативный функтор
Любой класс, у которого есть метод ap и который имплементирует спецификацию Applicative, называется аппликативным функтором. Аппликативные функторы используются в функциях, которые работают с возможными null-значениями в правой и левой части присваивания.
Оказывается, Maybe-монады также реализуют метод ap, и, следовательно, являются аппликативными функторами. Таким образом, мы можем использовать Maybe-монады для решения этой задачи.
Давайте взглянем на то, как заставить функцию 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: Передадим первый агрумент (maybeUser) в метод applyDiscount, используя map.
const maybeApplyDiscountFunc = maybeUser.map(applyDiscount);
// applyDiscount каррирована и функция map передает только один параметр, следовательно, возвращаемым результатом
// (maybeApplyDiscountFunc) будет функция, обернутая в монаду, которая хранит переменную maybeUser в замыкании
Шаг 4: Используем maybeApplyDiscountFunc.
Значение maybeApplyDiscountFunc может быть:
- функцией, обернутой в Maybe, если пользователь существует.
- Nothing, если пользователь равен null.
Если пользователь равен null, то при передаче второго аргумента в функцию, ничего не произойдет. Не выбросятся также и ошибки, связанные с нулевыми значениями.
В случае, когда пользователь существует, мы можем передать второй аргумент, используя map, чтобы запустить функцию:
maybeDiscount.map(maybeApplyDiscountFunc)! // Проблема!
Мы столкнулись с проблемой: map не знает, как запустить функцию, когда она обернута в Maybe-монаду.
В этом случае нам нужен другой метод, который умеет работать с обернутыми функциями. На помощь нам приходит метод ap.
Шаг 5: Используем функцию ap. Этот метод принимает монаду Maybe и выполняет функцию, хранящуюся внутри.
class Maybe {
constructor(val) {
this.val = val;
}
...
...
// реализация ap
ap(differentMayBe) {
return differentMayBe.map(this.val);
}
}
Применим метод ap:
maybeApplyDiscountFunc.ap(maybeDiscount)
Подведем итог: если у вас есть функция, которая работает с несколькими переменными, значения которых может быть null, вы должны сначала каррировать её, а затем обернуть в Maybe. Кроме этого, поместите все параметры в Maybe и используйте ap, чтобы запустить функцию.
Множественное каррирование
Мы уже познакомились с каррированием в прошлой части цикла. Оно позволяет передавать значения в функцию, которая принимает несколько аргументов, по одному.
// Пример каррирования
const add = (a, b) => a+b;
const curriedAdd = R.curry(add);
const add10 = curriedAdd(10); // Передаем первый аргумент. Нам возвращается функция, принимающая второй параметр.
// Вызываем функцию, передавая второй аргумент.
add10(2) // -> 12
Но что если у нас будет функция, которая может суммировать не два, а несколько аргументов?
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) // Возвращает функцию, которая принимает 2 параметра.
add3Numbers(1, 2) // Возвращает функцию, которая принимает один параметр.
Использование curryN для ожидания определенного количества вызовов функции.
Предположим, что мы хотим реализовать функцию, которая выводит лог только после 3х её вызовов.
// не чистая реализация
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 ожидает параметры
Пример 4. Сбор и отображение нескольких ошибок
Освещенные темы: Валидации (Валидационный функтор, Валидационный аппликативный функтор, Валидационная монада)
Валидации подобны монадам Either и используются в работе с композицией функций, выбрасывающих ошибки. Но, в отличие от Either, в котором для композиции используется метод chain, в валидационных монадах мы обычно используем метод ap. Также, в отличие от метода chain, который позволяет отобразить только первую ошибку, метод ap позволяет собрать массив из всех исключений.
Они обычно используются в валидациях форм, в которых все ошибки, возникшие при заполнении, показываются сразу же.
Сценарий использования: У нас есть форма регистрации, в которой валидируются имя пользователя, пароль и e-mail с помощью трех функций: isUsernameValid, isPwdLengthCorrect и isEmailValid. Мы должны показать одну, две или три ошибки в зависимости от введенных данных.
Давайте реализуем эту задачу, используя валидационный аппликативный функтор.
Мы будем использовать библиотеку data.validation из folktalejs, поскольку в ramda-fantasy еще не реализованы валидации.
У валидационного функтора есть два конструктора: 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: Создайте функцию-заглушку.
const returnSuccess = () => 'success'; // возвращает success
Шаг 3: Используйте curryN, чтобы повторно применить ap.
Проблема с функцией ap в том, что левая часть выражения должна быть функтором или монадой, содержащей функцию.
Например, предположим, что мы хотим повторно применить ap, как показано ниже. Это будет работать только в том случае, когда monad1 содержит функцию. Результат monad1.ap(monad2)
также должен быть монадой, содержащей функцию, чтобы мы могли использовать ap на monad3.
let finalResult = monad1.ap(monad2).ap(monad3)
// Может быть переписано, как:
let resultingMonad = monad1.ap(monad2)
let finalResult = resultingMonad.ap(monad3)
В нашем случае у нас есть 3 функции, которые нам надо применить
Давайте предположим, что мы сделали что-то вроде:
Success(returnSuccess)
.ap(isUsernameValid(username)) // сработает
.ap(isPwdLengthCorrect(pwd)) // не сработает
.ap(ieEmailValid(email)) // не сработает
Код выше не сработает, потому что Success(returnSuccess).ap(isUsernameValid(username))
вернет значение, и мы не сможем вызвать от него метод ap.
Мы можем использовать curryN, чтобы возвращать функцию, пока она не вызвана N раз.
function validateForm(username, pwd, email) {
let success = R.curryN(3, returnSuccess);
return Success(success)
.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';
function validateForm(username, pwd, email) {
let success = R.curryN(3, returnSuccess);
.ap(isPwdLengthCorrect(pwd))
.ap(ieEmailValid(email))
}
validateForm('raja', 'pwd1234567890', 'r@r.com').value;
// Вывод: success
validateForm('raja', 'pwd', 'r@r.com').value;
// Вывод: ['Password must be 10 characters' ]
validateForm('raja', 'pwd', 'notAnEmail').value;
// Вывод: ['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']
Перевод статьи «Functional Programming In JS With Practical Examples (Part 2)»