Функціональне програмування з прикладами на JavaScript. Частина друга. Аплікативні функтори, curryN і валідації


Дізнайтесь більше про нові кар'єрні можливості в 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 може бути:

  1. Функцією, обернутою в Maybe, якщо користувач є.
  2. 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 = /^ (([^<>()[].,;:[email protected]"]+(.[^<>()[].,;:[email protected]"]+)*)|(".+")) @ (([[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?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *