Функціональне програмування стало гарячою темою в світі JavaScript. Всього кілька років тому, мало хто з програмістів JavaScript знав що таке функціональне програмування. Сьогодні ж багато хто пробує застосовувати ці ідеї в свої проектах.
В даному пості я вирішив зібрати якомога більше теорії про ФП, особливо застосування його в JavaScript, і зрозуміти чому ця парадигма стає все більш популярною. Ріст її популярності добре видно на наступно графіку з Google Trends:
Вікіпедія каже що Функціональне програмування – розділ дискретної математики і парадигма програмування, в якій процес обчислення трактується як обчислення значень функцій в математичному розумінні останніх. Функціональне програмування передбачає обходитися обчисленням результатів функцій від вихідних даних і результатів інших функцій, і не передбачає явного зберігання стану програми. Відповідно, не передбачає воно і змінність цього стану (на відміну від імперативного, де однією з базових концепцій є змінна, що зберігає своє значення і дозволяє змінювати його в міру виконання алгоритму).
Це визначення звучить якось заумно, тому є інше визначення – Функціональне програмування (ФП) – це процес створення програмного забезпечення використовуючи чисті функції (pure functions), уникаючи загального стану (shared state), змінюваних даних (mutable data), а також побічних ефектів (side-effects).
Функціональне програмування – це парадигма програмування, а це означає, що це спосіб спосіб мислення в процесі створення програмного забезпечення на основі деяких фундаментальних принципів (перераховані вище). Основна ідея ФП наступна – використовуй змінюваний стан, тільки тоді, коли це дійсно необхідно.
Основні терміни:
- Функції вищих порядків (Higher-order Functions)
- Функції першого класу (First-Class Functions)
- Чисті функції (pure functions)
- Побічний ефект функції (side-effects)
- Незмінний стан (immutable state)
- Shared State
- Замикання (closure)
- Рекурсія
- Часткове застосування функції (Partial function)
- Функтор
- Монада
Функції вищих порядків (Higher-order Functions)
Функції вищих порядків (Higher-order Functions) – це такі функції, які можуть приймати в якості аргументів і повертати інші функції. Функції вищих порядків дозволяють використовувати каррінг (про це трохи пізніше) – перетворення функції від пари аргументів на функцію, що бере свої аргументи по одному. Оскільки в JavaScript функції – це об’єкти, то ми можемо їх легко передавати як аргументи іншим функціям і повертати як результат.
Ось простий приклад функції, яка повертає функцію:
function makeAdder(base) { return function(num) { return base + num; } }
І приклад її використання:
var add2 = makeAdder(2); add2(3); //5 add2(7); //9
А ось досить відомий приклад функції вищого порядку:
var el = document.getElementById("btn"); el.addEventListener("click", function (event){ });
addEventListener як параметр отримує функцію. Тобто addEventListener є функцією вищого порядку. Функція-обробник буде викликана, коли станеться подія click.
Функції першого класу (First-Class Functions)
Функції першого класу (First-Class Functions) – значить, що ви зможете зберігати функції в змінні. В JavaScript це використовується практично всюди:
var add = function (a, b) { return a + b }
Чисті функції (pure functions)
Чистими називають функції (pure functions), які не мають побічних ефектів, вони залежать тільки від своїх параметрів і повертають тільки свій результат. Чисті функції володіють декількома корисними властивостями, багато з яких можна використовувати для оптимізації коду:
- Якщо результат чистої функції не використовується, її виклик може бути видалений без шкоди для інших частин скріпта.
- Результат виклику чистої функції може бути збережений в таблиці значень разом з аргументами виклику. Якщо в подальшому функція викликається з цими ж аргументами, її результат може бути взятий прямо з таблиці, не вираховуючи значення заново (іноді це називається принципом прозорості посилань). Ціною невеликої витрати пам’яті можна істотно збільшити продуктивність і зменшити порядок зростання деяких рекурсивних алгоритмів.
- Якщо немає ніяких shared даних між двома чистими функціями, то порядок їх обчислення можна поміняти або распараллелить (інакше кажучи обчислення чистих функцій задовольняє принципам thread-safe).
//pure function var add = function (a, b) { return a + b; };
Чисті функції не створюють побічних ефектів. Ви передаєте в них якісь дані, і вони віддають вам дані назад. Їх дуже просто аналізувати. Їх легше тестувати. Не треба перевіряти зовнішні залежності. Тому в більшості випадків чисті функції краще функцій з побічними ефектами. Але, з іншого боку, програма яка складається виключно з чистих функцій не несе практичного сенсу. Вона нічого не зчитує і не виводить. Тому логічно буде писати програми таким чином, щоб відокремити чисті функції від функцій з побічними ефектами.
Побічний ефект функції (side-effect)
Побічний ефект функції (side-effect) – можливість в процесі виконання своїх обчислень: читати і модифікувати значення глобальних змінних, здійснювати операції введення-виведення, реагувати на виняткові ситуації, викликати їх обробників. Якщо викликати функцію з побічним ефектом двічі з одним і тим же набором значень вхідних аргументів, може статися так, що в якості результату будуть повернуті різні значення. Такі функції називаються недетермінованими функціями з побічними ефектами.
Незмінний стан (immutable state)
Незмінний стан (immutable state) означає, що ви взагалі не можете змінювати будь-які стани (хоча можете створювати нові).
var statement = "I am an immutable value"; var otherStr = statement.slice(8, 17);
Shared State
Shared State – це змінна, об’єкт або область пам’яті які існують в загальному скоупі (scope або області видимості) або властивість об’єкта яка може передаватись в інші області видимості. Shared scope може включати глобальний скоуп і closure скоуп.
// With shared state, the order in which function calls are made // changes the result of the function calls. const x = { val: 2 }; const x1 = () => x.val += 1; const x2 = () => x.val *= 2; x1(); x2(); console.log(x.val); // 6 // This example is exactly equivalent to the above, except... const y = { val: 2 }; const y1 = () => y.val += 1; const y2 = () => y.val *= 2; // ...the order of the function calls is reversed... y2(); y1(); // ... which changes the resulting value: console.log(y.val); // 5
Замикання (closure)
Замикання (closure) значить, що ви можете зберігати деякі дані всередині функції, які будуть доступні в специфічній функції що повертається, іншими словами повертаєма функція зберігає своє середовище виконання.
var add = function(a) { return function(b) { return a + b } } var add2 = add(2) add2(3) // => 5
Якщо придивитись до цього прикладу, то можна побачити що це та ж Функція вищого порядку, змінна a була замкнута і доступна тільки в повертаємій функції.
Рекурсія
Рекурсія – можливість функції викликати саму себе. У функціональних мовах цикл зазвичай реалізується у вигляді рекурсії. В функціональній парадигмі програмування немає такого поняття, як цикл. Рекурсивні функції викликають самі себе, дозволяючи операції виконуватися знову і знову.
function factorial(num) { // If the number is less than 0, reject it. if (num < 0) { return -1; } // If the number is 0, its factorial is 1. else if (num == 0) { return 1; } // Otherwise, call this recursive procedure again. else { return (num * factorial(num - 1)); } } var result = factorial(8);
Часткове застосування функції (Partial function)
Часткове застосування функції (Partial function) – створення якоїсь функції на базі зазначеної, де деякі аргумент замінені конкретними значеннями. наприклад:
function partial_one_arg(f, a) { return function(b) { return f(a, b); } } min0 = partial_one_arg(Math.min, 0); min1 = partial_one_arg(Math.min, 1); alert([min0(-5), min0(5)]); // вернёт [-5, 0] alert([min1(-5), min1(5)]); // вернёт [-5, 1]
В JavaScript для реалізації часткового застосування можна використовувати Function.prototype.bind. Цей метод використовується, в більшості випадків, коли необхідно позбутися від надмірних присвоювання var that = this або var self = this, що зустрічаються в коді на кожному кроці. Ось простий приклад:
this.setup = function () { this.on('event', this.handleEvent.bind(this)); };
Перший аргумент, переданий методу, служить в якості this в рамках функції, яку повертає bind. Кожен наступний після першого параметр bind додається в початок списку параметрів при виклику прив’язаною функції.
Це означає, що ми можемо створити часткове застосування функції наступного вигляду:
var add = function (a, b) { return a + b; }; var add2 = add.bind(null, 2); add2(10) === 12;
Функтори
Функтор – це будь-клас або тип даних, який зберігає значення і реалізує метод map.
Наприклад, Array – це функтор, тому що масив зберігає значення і реалізує метод map, що дозволяє нам застосовувати функцію до значень, які він зберігає.
Детальніше про функтори можна прочитати в попредній статті – Основні техніки функціонального програмування
Монада
Монада – це підтип функторів, так як у них є метод map, але вони також реалізують інші методи, наприклад, ap, of, chain.
Детальніше про монади можна прочитати в попредній статті – Основні техніки функціонального програмування
Переваги функціонального програмування:
Підвищення надійності коду
Приваблива сторона обчислень без стану – підвищення надійності коду за рахунок чіткої структуризації та відсутності необхідності відстеження побічних ефектів. Будь-яка функція працює тільки з локальними даними і працює з ними завжди однаково, незалежно від того, де, як і за яких обставин вона викликається. Неможливість мутації даних при користуванні ними в різних місцях програми виключає появу помилок яких важко знайти (таких, наприклад, як випадкове присвоювання невірного значення глобальної змінної в імперативній програмі).
Зручність організації модульного тестування
Оскільки функція у функціональному програмуванні не може породжувати побічні ефекти, змінювати об’єкти не можна як усередині області видимості, так і зовні (на відміну від імперативних програм, де одна функція може встановити якусь зовнішню змінну, що зчитується другою функцією). Єдиним ефектом від обчислення функції є повертаємий нею результат, і єдиний фактор, який впливає на результат – це значення аргументів.
Таким чином, є можливість протестувати кожну функцію в програмі, просто обчисливши її з різними наборами значень аргументів. При цьому можна не турбуватися ні про виклик функцій в правильному порядку, ні про правильне формування зовнішнього стану. Якщо будь-яка функція в програмі проходить модульні тести, то можна бути впевненим в якості всієї програми. В імперативних програмах перевірки повертаємого значення недостатньо: функція може модифікувати зовнішній стан, який теж потрібно перевіряти, чого не потрібно робити в функціональних программаx.
Матеріали для вивчення ФП:
Функциональное программирование должно стать вашим приоритетом №1
Paradigms of Computer Programming – Fundamentals
Функциональное программирование для всех
Функциональное программирование на Javascript
Master the JavaScript Interview: What is Functional Programming?
ФУНКЦІОНАЛЬНЕ ПРОГРАМУВАННЯ. ОСНОВНІ ТЕХНІКИ ФУНКЦІОНАЛЬНОГО ПРОГРАМУВАННЯ. ЧАСТИНА 1.