У минулій частині ми розглянули основні інструменти функціонального програмування з прикладами на 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 може бути:

  1. функцією, оберненої в Maybe, якщо користувач існує.
  2. Nothing, якщо користувач дорівнює null.
  3. Якщо користувач дорівнює 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 = () =&amp;gt; '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)»

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

Схожі статті

Leave a Reply

Your email address will not be published. Required fields are marked *