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, wpływa na poprawę jakości kodu, pozwala uzyskać rozszerzalną i przejrzystą strukturę aplikacji oraz umożliwia wielokrotne wykorzystywanie kodu. W tym wpisie zapoznamy się z podstawowymi konceptami programowania funkcyjnego w kontekście języka JavaScript. Dowiemy się, dlaczego programowanie funkcyjne jest istotne, jakie korzyści niesie ze sobą i jak możemy je wykorzystać w naszych projektach.

Rozpoczniemy od omówienia podstawowych idei programowania funkcyjnego, takich jak czystość funkcji, niemutowalność danych i funkcje wyższego rzędu. Przeanalizujemy również, jak w praktyce wykorzystać funkcje anonimowe, funkcje strzałkowe oraz kompozycję funkcji.

Programowanie Funkcyjne JavaScript

Czym jest programowanie funkcyjne i dlaczego jest istotne

Przede wszystkim, programowanie funkcyjne nie polega wyłącznie na podziale programu na funkcje. Jego głównym celem jest abstrakcyjne podejście do operacji na danych za pomocą funkcji, w celu uniknięcia efektów ubocznych oraz ograniczenia modyfikacji stanu w naszej aplikacji. Choć termin programowania funkcyjnego nie jest nowy i istnieje od dawna jako paradygmat programowania, to w ostatnim czasie zyskał ogromną popularność, a na to znacznie wpłynęła biblioteka React.

Programowanie funkcyjne na początku wymaga przyswojenia kilku terminów i zrozumienia pewnych koncepcji, jednak niewątpliwie warto poświęcić czas na naukę tego paradygmatu. Według wielu najlepsze rezultaty można uzyskać dzięki połączeniu paradygmatów – obiektowego i funkcyjnego. Moim zdaniem to prawda. Szalenie ważne jest jednak zachowanie równowagi – nadużycie programowania funkcyjnego bardzo szybko może sprawić, że nasz kod będzie mniej czytelny.

Funkcje Pierwszej Klasy i Funkcje Wyższego Rzędu

W języku JavaScript, prawie wszystko, włączając w to funkcje, jest traktowane jako obiekt. Dzięki temu możemy traktować funkcje jak zwykłe zmienne i wykorzystywać je na wiele sposobów, np. przekazywać jako argumenty do innych funkcji. Tego rodzaju funkcje nazywane są funkcjami pierwszej klasy (first-class function).

W skrócie, w JavaScript funkcje są traktowane jako wartości pierwszej klasy, co oznacza, że mogą być przypisywane do zmiennych, przekazywane jako argumenty do innych funkcji i zwracane jako wynik z innych funkcji. Dzięki temu funkcje stają się elastycznymi narzędziami do manipulacji danymi, co jest jednym z głównych założeń programowania funkcyjnego.

Oto przykład kodu, który demonstruje przypisywanie funkcji do zmiennych:

// Definiowanie funkcji
function powitanie() {
  console.log("Witaj!");
}

// Przypisywanie funkcji do zmiennej
const przywitaj = powitanie;

// Wywołanie funkcji przez zmienną
przywitaj(); // Wyświetli: "Witaj!"

W powyższym przykładzie funkcja powitanie jest przypisana do zmiennej przywitaj i może być wywoływana za pomocą tej zmiennej.

Z kolei funkcje wyższego rzędu (higher-order function) to takie, które przyjmują inne funkcje jako argumenty lub zwracają funkcje.

W programowaniu funkcyjnym funkcje wyższego rzędu są niezwykle użyteczne, ponieważ pozwalają nam wyeliminować imperatywne struktury, takie jak pętle, oraz uniknąć powtarzalnych fragmentów kodu. Przykładem funkcji wyższego rzędu w JavaScript są metody takie jak .map(), .reduce() czy .forEach(), które operują na kolekcjach danych.

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, co z kolei przekłada się na mniejszą ilość błędów.

Przykład kodu imperatywnego:

// Deklaracja i inicjalizacja tablicy
var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// Pętla iterująca przez tablicę
for (let i = 0; i < array.length; i++) {
  // Podniesienie elementu tablicy do kwadratu przy użyciu funkcji Math.pow()
  array[i] = Math.pow(array[i], 2);
}

Kod inicjalizuje tablicę array zawierającą liczby od 0 do 9. Następnie, za pomocą pętli for, iteruje przez tablicę i za każdym razem podnosi element do kwadratu, korzystając z funkcji Math.pow(). W rezultacie, elementy tablicy zostaną zastąpione ich kwadratami.

Dla porównania, ten sam kod w wersji deklaratywnej:

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

// Wykorzystanie metody map() do przekształcenia elementów tablicy
const squaredArray = array.map((num) => Math.pow(num, 2));

W powyższym kodzie, zamiast pętli for, używamy metody map() na tablicy array, która iteruje przez wszystkie elementy i zwraca nową tablicę, w której każdy element jest podniesiony do kwadratu. Funkcja przekazana do metody map() jest używana do przekształcenia każdego elementu zgodnie z określoną logiką. W tym przypadku, wykorzystujemy funkcję strzałkową, aby podnieść każdy element do kwadratu przy użyciu funkcji Math.pow().

Funkcje powinny być czyste

Czystość funkcji to kolejny ważny koncept w programowaniu funkcyjnym. Czyste funkcje to takie, 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 oraz nie ma żadnych efektów ubocznych, czyli nie zmienia stanu programu ani nie korzysta z zewnętrznych zmiennych. O takich funkcjach mówimy, że są przejrzyste referencyjnie.

Przykład funkcji, która nie jest przejrzysta referencyjnie:

let counter = 0;

function increment() {
  ++counter;
}

Powyższa funkcja increment nie jest czysta, ponieważ modyfikuje zmienną poza swoim kontekstem.

Funkcja przejrzysta referencyjnie:

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

Funkcja increment jest czysta, ponieważ zawsze zwraca taki sam wynik dla tych samych argumentów, niezależnie od kontekstu wywołania.

Ponownie, jest bardzo prosty przykład, jednak chodzi o zobrazowanie idei i przyswojenie pewnych terminów. Czyste funkcje zwiększają czytelność kodu i zmniejszają ryzyko pojawienia się błędów.

Niezmienność danych

W programowaniu funkcyjnym stawia się również duży nacisk na niemutowalność danych, czyli unikanie bezpośrednich zmian w istniejących strukturach danych. Zamiast tego, preferuje się tworzenie nowych struktur danych na podstawie istniejących.

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:

// Deklaracja i inicjalizacja tablicy
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

// Funkcja sortująca tablicę w porządku malejącym
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]

Kod inicjalizuje tablicę arr zawierającą liczby od 1 do 9. Następnie, zdefiniowana jest funkcja sortDesc, która sortuje tablicę w porządku malejącym, korzystając z metody sort() i porównując elementy a i b. Funkcja sortDesc zwraca posortowaną tablicę.

W kolejnych linijkach kodu, wywołujemy funkcję sortDesc na tablicy arr, co powoduje sortowanie tablicy w miejscu (zmianę oryginalnej tablicy). Wynik sortowania to tablica [9,8,7,6,5,4,3,2,1]. Następnie, wyświetlamy zawartość tablicy arr, która również uległa zmianie i wynosi [9,8,7,6,5,4,3,2,1].

Sortowanie bez mutacji danych:

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

const sortDesc = arr => {
  // Utworzenie nowej tablicy z posortowanymi wartościami w porządku malejącym
  const sortedArr = [...arr].sort((a, b) => b - a);
  return sortedArr;
};

const sortedArray = sortDesc(arr); //-> [9,8,7,6,5,4,3,2,1]
console.log(sortedArray);

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

W powyższym kodzie, zamiast modyfikować oryginalną tablicę arr, tworzymy nową tablicę sortedArr przy użyciu operatora rozproszenia [...arr], aby zachować niemutowalność danych. Następnie, na tej nowej tablicy stosujemy funkcję sort() z odpowiednią logiką porównywania elementów, aby uzyskać posortowaną tablicę w porządku malejącym.

W rezultacie, wywołanie funkcji sortDesc(arr) zwraca nową tablicę [9,8,7,6,5,4,3,2,1], która zawiera posortowane wartości. Oryginalna tablica arr pozostaje niezmieniona i nadal zawiera pierwotne wartości [1,2,3,4,5,6,7,8,9].

Praca z funkcjami wyższego rzędu w JavaScript

Funkcje wyższego rzędu są kluczowym elementem programowania funkcyjnego w języku JavaScript. Są to funkcje, które przyjmują inną funkcję jako argument lub zwracają funkcję jako wynik. Dzięki temu programiści mogą tworzyć bardziej elastyczne i modularne rozwiązania. Praca z funkcjami wyższego rzędu umożliwia operacje na funkcjach, takie jak przekazywanie ich jako parametry, tworzenie funkcji w locie i komponowanie ich w bardziej złożone struktury.

Oto kilka przykładów kodu, które ilustrują pracę z funkcjami wyższego rzędu w JavaScript:

Przekazywanie funkcji jako argument

// Funkcja wyższego rzędu, przyjmująca funkcję jako argument
function executeCallback(callback) {
  // Wywołanie przekazanej funkcji
  callback();
}

// Przykładowa funkcja, która może zostać przekazana jako argument
function greet() {
  console.log('Hello, world!');
}

// Wywołanie funkcji wyższego rzędu z przekazaną funkcją jako argument
executeCallback(greet); // Wyświetli: Hello, world!

W tym przykładzie executeCallback jest funkcją wyższego rzędu, która przyjmuje funkcję callback jako argument. Przekazana funkcja greet jest następnie wywoływana wewnątrz funkcji executeCallback, co powoduje wyświetlenie wiadomości „Hello, world!”.

Zwracanie funkcji jako wynik

// Funkcja wyższego rzędu, zwracająca funkcję jako wynik
function createMultiplier(factor) {
  // Zwracanie funkcji, która mnoży podaną wartość przez czynnik
  return function(value) {
    return value * factor;
  };
}

// Użycie funkcji wyższego rzędu do utworzenia funkcji mnożącej przez 2
const multiplyBy2 = createMultiplier(2);

// Wywołanie zwróconej funkcji
console.log(multiplyBy2(5)); // Wyświetli: 10

W tym przypadku createMultiplier jest funkcją wyższego rzędu, która zwraca inną funkcję. Zwrócona funkcja przyjmuje wartość i mnoży ją przez czynnik przekazany do createMultiplier. W przykładzie powyżej funkcja multiplyBy2 jest wynikiem wywołania createMultiplier(2), co oznacza, że mnoży podaną wartość przez 2. Wywołanie multiplyBy2(5) zwraca wartość 10.

Praca z funkcjami wyższego rzędu w JavaScript pozwala na tworzenie bardziej elastycznego i modularnego kodu. Funkcje te otwierają drzwi do zaawansowanych technik programowania funkcyjnego, takich jak kompozycja funkcji.

Praca z listami i tablicami w programowaniu funkcyjnym

Na koniec zobaczymy kilka praktycznych zastosowań dla wbudowanych funkcji wyższego rzędu.

Listy i tablice są powszechnie wykorzystywanymi strukturami danych w programowaniu. W programowaniu funkcyjnym istnieją różne techniki i metody, które umożliwiają pracę z listami i tablicami w sposób funkcyjny i deklaratywny.

Oto przykład kodu, który demonstruje prace z listami i tablicami w programowaniu funkcyjnym w języku JavaScript:

// Przykładowa tablica z liczbami
const numbers = [1, 2, 3, 4, 5];

// Mapowanie - zastosowanie funkcji do każdego elementu tablicy
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // Wyświetli: [1, 4, 9, 16, 25]

// Filtracja - wybieranie elementów spełniających określone kryterium
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Wyświetli: [2, 4]

// Redukcja - łączenie wszystkich elementów w jedną wartość
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // Wyświetli: 15

// Iteracja - wykonanie operacji dla każdego elementu tablicy
numbers.forEach(num => {
  console.log(num); // Wyświetli każdą liczbę osobno
});

W powyższym kodzie mamy tablicę numbers, która zawiera kilka liczb. Następnie, wykorzystujemy różne metody i techniki programowania funkcyjnego do manipulacji tą tablicą.

  • Metoda map pozwala zastosować określoną funkcję do każdego elementu tablicy i zwraca nową tablicę z wynikami. W przykładzie wykorzystujemy map, aby podnieść każdą liczbę do kwadratu.
  • Metoda filter pozwala wybrać elementy, które spełniają określone kryterium, i zwraca nową tablicę zawierającą te elementy. W przykładzie wykorzystujemy filter, aby wybrać tylko parzyste liczby.
  • Metoda reduce pozwala na łączenie wszystkich elementów tablicy w jedną wartość poprzez zastosowanie określonej funkcji redukującej. W przykładzie używamy reduce, aby obliczyć sumę wszystkich liczb w tablicy.
  • Metoda forEach wykonuje określone operacje dla każdego elementu tablicy. W przykładzie używamy forEach, aby wyświetlić każdą liczbę osobno.

Dzięki tym technikom programowania funkcyjnego możemy bardziej deklaratywnie operować na listach i tablicach, tworząc czytelny i ekspresywny kod.

Podsumowanie – Programowanie Funkcyjne w JavaScript

W tym wpisie na bloga przeprowadziliśmy kompleksowe wprowadzenie do programowania funkcyjnego w języku JavaScript. Zapoznaliśmy się z podstawowymi konceptami i zaletami tego podejścia programistycznego.

Początkowo nauka programowania funkcyjnego może wydawać się trudna. Jednak kiedy nowe podejście wejdzie w krew, zaczyna się doceniać korzyści płynące z tego paradygmatu. Naturalnym staje się korzystanie z metod, takich jak .map() czy .filter(). Programowanie funkcyjnie nie tylko sprawia, że nasz kod staje się lepszy, ale przede wszystkim daje dużo satysfakcji.

Programowanie funkcyjne w JavaScript to potężne narzędzie, które może poprawić jakość, czytelność i skalowalność naszego kodu. Warto eksperymentować z tym podejściem i wykorzystać jego zalety w naszych projektach. Mam nadzieję, że ten wpis dostarczył Ci solidnego fundamentu i inspiracji do dalszego poznawania programowania funkcyjnego w języku JavaScript. Jeśli chcesz dowiedzieć się więcej, zapraszam Cię do kolejnej części wpisu o zaawansowanych technikach programowania funkcyjnego w JavaScript.


3 odpowiedzi na “JavaScript – Programowanie Funkcyjne”

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *