Функціональне програмування стало гарячою темою в світі 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.

Схожі статті