JavaScript – Programowanie Funkcyjne

Programowanie funkcyjne jest drugim, obok obiektowego, z najważniejszych paradygmatów języka JavaScript. W podejściu obiektowym zrozumienie kodu uzyskuje się m.in. dzięki hermetyzacji zmieniających elementów.

Programowanie funkcyjne ułatwia zrozumienie kodu dzięki minimalizowaniu liczby takich elementów. Dzięki podziałowi zadań na proste funkcje oraz mniejszej złożoności kodu, wpływa na poprawę jakości kodu, pozwala uzyskać rozszerzalną i przejrzystą strukturę aplikacji oraz umożliwia wielokrotne wykorzystywanie kodu.

Zacznij myśleć funkcyjnie

Celem programowania funkcyjnego nie jest podział programu na funkcje. Celem jest abstrakcyjne ujęcie operacji na danych za pomocą funkcji, tak aby uniknąć efektów ubocznych i ograniczyć modyfikacje stanu w aplikacji. Programowanie funkcyjne jest powszechnie wykorzystywane i spotkać je możemy np. w bibliotece React, która czerpie garściami z tego paradygmatu programowania.

Warto dlatego przyswoić sobie podstawowe terminy związane z programowaniem funkcyjnym, co najlepiej zrobić na przykładach.

Programowanie funkcyjne jest deklaratywne

W przypadku programowania deklaratywnego opisujemy co chcemy osiągnąć (jakie warunki musi spełniać rozwiązanie końcowe). Uzyskuje się to m.in. poprzez unikanie imperatywnych struktur sterujących (pętli), które są trudne do ponownego wykorzystania i scalania z innymi operacjami. Co ważne, podejście deklaratywne wpływa na minimalizację skutków ubocznych.

Przykład kodu imperatywnego:

var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (let i = 0; i < array.length; i++) {
  array[i] = Math.pow(array[i], 2);
}

array; //-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Przykład kodu deklaratywnego:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => Math.pow(num, 2));
//-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Funkcje powinny być czyste

Czyste funkcje to funkcje, które nie wykonują zmian poza swoim zasięgiem. Ich wynik zależy od danych wejściowych, a nie zewnętrznego stanu. Funkcja taka zwraca ten sam wynik dla tych samych danych wejściowych. O takich funkcjach mówimy, że są przejrzyste referencyjnie.

Przykład funkcji, która nie jest czysta:

let counter = 0;

function increment() {
  return ++counter;
}

Przykład czystej funkcji:

function increment(counter) {
  return counter++;
}

W rzeczywistości nie da się w pełni wyeliminować zmian stanu. W praktyce programowanie funkcyjne pomaga nimi zarządzać, ograniczyć ich liczbę oraz oddzielić czyste funkcje od pozostałych.

Niemodyfikowalność danych

Niemodyfikowalne dane to takie, które nie mogą być zmieniane po ich utworzeniu. Dzięki takiemu podejściu zmniejszamy ryzyko pojawiania się błędów oraz niepożądanych skutków ubocznych.

Przykład mutacji danych:

const arr = [1,2,3,4,5,6,7,8,9];

const sortDesc = arr => {
  return arr.sort(
    (a, b) => b - a
  );
}

sortDesc(arr); //-> [9,8,7,6,5,4,3,2,1]
arr; //-> [9,8,7,6,5,4,3,2,1]

Bez mutowania danych:

const arr = [1,2,3,4,5,6,7,8,9];

const sortDesc = arr => {
  return [...arr].sort( // w tym miejscu tworzymy nową tablicę
    (a, b) => b - a
  );
}

sortDesc(arr); //-> [9,8,7,6,5,4,3,2,1]
arr; //-> [1,2,3,4,5,6,7,8,9]