Планета гаджетов / технологий
Содержание
В прошлой части мы рассмотрели основные инструменты функционального программирования с примерами на JavaScript: монады, функторы, каррирование. В этой статье мы закончим обзор принципов и инструментов, которые помогут вам построить поистине чистое приложение в функциональном стиле.
Используемые концепты ФП: Аппликативные функторы.
Сценарий использования: Предположим, что мы хотим предоставить скидку пользователю, если пользователь залогинен и у нас есть действующее предложение (т.е. существует скидка).
Для этой задачи мы будем использовать метод applyDiscount, представленный ниже. Этот метод может выбрасывать ошибки типа null в случае, если пользователь или скидка равняется null.
// Предлагает пользователю скидку, если и пользователь, и скидка существуют // Выбрасывает ошибку, если пользователь или скидка равны null const applyDiscount = (user, discount) => { let userClone = clone(user); // используем какую-нибудь библиотеку, чтобы создать копию объекта userClone.discount = discount.code; |
Давайте посмотрим, как мы можем решить эту задачу, используя аппликативные функторы.
Любой класс, у которого есть метод 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 может быть:
Если пользователь равен null, то при передаче второго аргумента в функцию, ничего не произойдет. Не выбросятся также и ошибки, связанные с нулевыми значениями.
В случае, когда пользователь существует, мы можем передать второй аргумент, используя map, чтобы запустить функцию:
maybeDiscount.map(maybeApplyDiscountFunc)! // Проблема! |
Мы столкнулись с проблемой: map не знает, как запустить функцию, когда она обернута в Maybe-монаду.
В этом случае нам нужен другой метод, который умеет работать с обернутыми функциями. На помощь нам приходит метод ap.
Шаг 5: Используем функцию ap. Этот метод принимает монаду Maybe и выполняет функцию, хранящуюся внутри.
// реализация ap 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); // Передаем первый аргумент. Нам возвращается функция, принимающая второй параметр. // Вызываем функцию, передавая второй аргумент. |
Но что если у нас будет функция, которая может суммировать не два, а несколько аргументов?
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 параметра. add3Numbers(1, 2) // Возвращает функцию, которая принимает один параметр. |
Использование curryN для ожидания определенного количества вызовов функции.
Предположим, что мы хотим реализовать функцию, которая выводит лог только после 3х её вызовов.
// не чистая реализация const logAfter3Calls = () => { console.log(‘called me 3 times’); logAfter3Calls() // Ничего не происходит logAfter3Calls() // Ничего не происходит logAfter3Calls() // ‘called me 3 times’ |
Мы можем написать эту функцию в функциональном стиле, используя curryN:
// Чистая реализация console.log(‘called me 3 times’); const logAfter3Calls = R.curryN(3, log); logAfter3Calls(»)(»)(») // ‘called me 3 times’ // Мы передаем » в качестве аргумента, т.к. curryN ожидает параметры |
Освещенные темы: Валидации (Валидационный функтор, Валидационный аппликативный функтор, Валидационная монада)
Валидации подобны монадам 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 функции, которые нам надо применить
Давайте предположим, что мы сделали что-то вроде:
.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; 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)»Антон Корольков, full stack ньюсрайтер