
Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.
У першій частині ми розглянули основні інструменти функціонального програмування з прикладами на JavaScript: монади, функтори, карирування. У цій статті ми завершимо огляд принципів та інструментів, які допоможуть Вам побудувати справді чистий додаток у функціональному стилі.
Приклад 3. Завдання значень об’єктам, які можуть дорівнювати Null
Використовувані концепти ФП : Аплікативні функтори.
Сценарій використання: Припустимо, що ми хочемо надати знижку користувачеві, якщо користувач залогінений і у нас є чинна пропозиція (є знижка).
Для цієї задачі ми використовуватимемо метод applyDiscount, представлений нижче. Цей метод може виводити помилки типу null у разі, якщо користувач або знижка дорівнює null.
// Пропонує користувачеві знижку, якщо і користувач, і знижка є // Виводить помилку, якщо користувач або знижка дорівнюють nullconst 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) // 6add3numbers (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') // з folktalejsconst Success = Validation.Successconst Failure = Validation.Failureconst 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') // з folktalejsconst Success = Validation.Successconst Failure = Validation.Failureconst 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;// Висновок: successvalidateForm ('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) “
Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?