Kurs JavaScript i TypeScript · Moduł 12: Programowanie funkcyjne
Czym jest programowanie funkcyjne?
W tej lekcji6
Poranny raport z wybiegu raptorów mówi: 12 osobników. Wieczorny, wygenerowany tą samą funkcją z tych samych danych: 13. Nic się nie wykluło, nic nie uciekło - jakaś funkcja po cichu zmieniła wspólny licznik. Takich błędów nie da się powtórzyć na zawołanie, a programowanie funkcyjne usuwa im grunt spod nóg.
Wyobraź sobie laboratorium w Parku Jurajskim, w którym każdy eksperyment na DNA dinozaura daje zawsze ten sam rezultat przy tych samych danych wejściowych. Nie ma żadnych efektów ubocznych - żaden dinozaur nie ucieka, żaden system bezpieczeństwa nie zostaje przypadkowo wyłączony. Dokładnie tak działa programowanie funkcyjne (FP) - to paradygmat, w którym kod zachowuje się jak kontrolowany eksperyment naukowy.
Czego się nauczysz
- odróżniać czyste funkcje od nieczystych,
- przetwarzać tablice próbek przez
map,filterireduce, - składać funkcje w linie przetwarzania z
pipeicompose, - konfigurować funkcje etapami i zamykać dane w domknięciach,
- przechodzić drzewo gatunków rekurencją i zmieniać dane bez mutacji,
- obsługiwać brakujące dane i błędy dzięki Maybe i Either,
- zbudować funkcyjny system raportów bezpieczeństwa parku.
Paradygmat funkcyjny vs imperatywny
W programowaniu imperatywnym mówimy komputerowi jak ma coś zrobić krok po kroku. W programowaniu funkcyjnym opisujemy co chcemy osiągnąć, używając funkcji jako podstawowych jednostek budulcowych.
Zadanie: z listy gatunków wybieramy drapieżniki. Najpierw klasyczna pętla, potem wersja funkcyjna:
1// Podejście imperatywne - krok po kroku
2const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops', 'Stegosaurus'];
3const carnivores = [];
4for (let i = 0; i < dinosaurs.length; i++) {
5 if (dinosaurs[i] === 'T-Rex' || dinosaurs[i] === 'Velociraptor') {
6 carnivores.push(dinosaurs[i]);
7 }
8}
9
10// Podejście funkcyjne - deklaratywne
11const isCarnivore = (name) => ['T-Rex', 'Velociraptor'].includes(name);
12const carnivoresFP = dinosaurs.filter(isCarnivore);Obie wersje dają ['T-Rex', 'Velociraptor']. W pierwszej sami pilnujemy licznika i, warunku pętli i dopisywania wyników przez push. W drugiej tylko nazywamy regułę: isCarnivore sprawdza metodą includes, czy nazwa jest na liście drapieżników, a przejście po tablicy zlecamy metodzie filter, która zwraca nową tablicę z pasującymi elementami. Co się nie zmieniło? Tablica dinosaurs w obu wersjach zostaje nietknięta.
Czyste funkcje (Pure Functions)
Czysta funkcja to kontrolowany eksperyment - przy tych samych danych wejściowych zawsze zwraca ten sam wynik i nie powoduje żadnych efektów ubocznych.
Zasady czystej funkcji:
- Determinizm - ten sam input = ten sam output, zawsze
- Brak efektów ubocznych - nie modyfikuje niczego poza swoim zakresem
Pierwsza funkcja poniżej liczy poziom zagrożenia wyłącznie z trzech argumentów. Druga robi coś, co wygląda niewinnie: przy okazji podbija licznik zadeklarowany poza nią.
1// CZYSTA FUNKCJA - zawsze ten sam wynik
2function calculateDangerLevel(weight, speed, aggression) {
3 return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
4}
5
6// Ta sama funkcja wywołana 1000 razy z tymi samymi argumentami
7// zawsze zwróci ten sam wynik
8calculateDangerLevel(8000, 40, 95); // zawsze 2450
9calculateDangerLevel(8000, 40, 95); // zawsze 2450
10
11// NIECZYSTA FUNKCJA - modyfikuje dane zewnętrzne
12let totalDinosaurs = 0;
13function addDinosaur(name) {
14 totalDinosaurs++; // efekt uboczny - zmienia zmienną zewnętrzną!
15 return { name, id: totalDinosaurs };
16}
17// Każde wywołanie daje inny wynik, nawet z tym samym argumentem
18addDinosaur('Rex'); // { name: 'Rex', id: 1 }
19addDinosaur('Rex'); // { name: 'Rex', id: 2 } - inny wynik!Wywołanie calculateDangerLevel(8000, 40, 95) to 2400 + 12 + 38, czyli 2450 - dziś, jutro i przy tysięcznym powtórzeniu. Z addDinosaur jest inaczej: ten sam argument 'Rex' daje dwa różne wyniki, bo funkcja zależy od zmiennej totalDinosaurs, którą sama zmienia. To mechanizm błędu z porannego raportu.
Efekt uboczny to każda zmiana świata poza funkcją: modyfikacja zmiennej zewnętrznej lub przekazanego obiektu, wpis w konsoli, zapytanie do serwera, zmiana zawartości strony. Determinizm łamie z kolei każde źródło zmienności, na przykład losowanie:
1// Nieczysta: efekt uboczny w postaci wpisu w konsoli
2function logSample(id) {
3 console.log('Próbka:', id);
4 return id;
5}
6
7// Nieczysta: wynik zależy od losowania, nie od argumentów
8function randomZone() {
9 return Math.random() > 0.5 ? 'A' : 'B';
10}Żadna z nich nie rusza danych parku, a mimo to obie są nieczyste: logSample zostawia ślad w konsoli, a randomZone bez argumentów zwraca raz 'A', raz 'B'. Tak samo nieczysta jest każda funkcja odczytująca czas przez Date.now().
Niemutowalność (Immutability)
Niemutowalność to zasada, że raz utworzone dane nigdy nie są zmieniane. Zamiast modyfikować istniejący obiekt, tworzymy nową kopię ze zmianami. To jak tworzenie nowej sekwencji DNA zamiast modyfikowania istniejącej - bezpieczniej i bardziej przewidywalnie.
Porównaj dwa sposoby odnotowania, że Rex został ranny:
1// MUTACJA - niebezpieczna (zmiana oryginalnego obiektu)
2const dinosaur = { name: 'Rex', health: 100 };
3dinosaur.health = 80; // Oryginalny obiekt został zmieniony!
4
5// NIEMUTOWALNOŚĆ - bezpieczna (nowy obiekt)
6const dinosaurSafe = { name: 'Rex', health: 100 };
7const injuredDinosaur = { ...dinosaurSafe, health: 80 };
8// dinosaurSafe wciąż ma health: 100 (niezmieniony)
9// injuredDinosaur ma health: 80 (nowy obiekt)Operator spread (...) kopiuje wszystkie pola do nowego obiektu, a health: 80 zapisane po nim nadpisuje jedno pole kopii. Zauważ też, że const nie uchronił obiektu dinosaur przed zmianą: blokuje ponowne przypisanie zmiennej, a nie zawartość obiektu. Do tej różnicy wrócimy w lekcji o wzorcach niemutowalności.
Funkcje jako wartości pierwszoklasowe
W JavaScript funkcje są wartościami pierwszoklasowymi (first-class citizens) - możesz je przypisywać do zmiennych, przekazywać jako argumenty i zwracać z innych funkcji.
Najpierw dwie pierwsze możliwości: funkcję zapisujemy w stałej, a potem przekazujemy ją do filter, tak jak próbkę przekazuje się między stanowiskami:
1// Funkcja przypisana do zmiennej
2const classify = (dino) => dino.diet === 'carnivore' ? 'dangerous' : 'safe';
3
4// Funkcja jako argument
5const dinosaurs = [
6 { name: 'Rex', diet: 'carnivore' },
7 { name: 'Brachio', diet: 'herbivore' },
8];
9const dangerous = dinosaurs.filter((dino) => classify(dino) === 'dangerous');Przypisanie classify niczego nie uruchamia - to przepis, który filter wywoła dla każdego dinozaura. W tablicy dangerous zostaje sam Rex.
Trzecia możliwość jest najciekawsza, bo funkcja może zbudować i zwrócić nową funkcję:
1// Funkcja zwracająca funkcję
2function createValidator(minWeight) {
3 return (dino) => dino.weight >= minWeight;
4}
5const isHeavyDino = createValidator(5000);
6isHeavyDino({ weight: 8000 }); // true
7isHeavyDino({ weight: 200 }); // falsecreateValidator(5000) produkuje gotowy walidator, który pamięta próg minWeight, choć wywołanie fabryki dawno się zakończyło. Funkcje przyjmujące lub zwracające inne funkcje nazywamy funkcjami wyższego rzędu, a mechanizm pamiętania zmiennych to domknięcie - oba tematy rozwiniemy w kolejnych lekcjach.
Dlaczego warto?
Czystą funkcję testujesz bez przygotowywania świata wokół niej: podajesz argumenty i porównujesz wynik - pamiętasz Jest z poprzedniej lokacji? Jej wynik możesz też bezpiecznie zapamiętać albo połączyć ją z innymi w łańcuch. Programu całkiem bez efektów ubocznych jednak nie napiszesz, bo ktoś musi wyświetlić raport. Dlatego polecam Ci zasadę Dr. Rexa: efekty trzymaj na brzegach programu (wczytanie danych na początku, console.log i zapis na końcu), a logikę w środku pisz jako czyste funkcje.
W edytorze poniżej zmień argumenty i sprawdź, które funkcje zachowują się jak kontrolowany eksperyment.
Programowanie funkcyjne sprowadza się do jednej obietnicy: te same dane na wejściu zawsze dają ten sam wynik, żadna próbka nie jest po cichu modyfikowana, a funkcje możesz przekazywać dalej tak samo swobodnie jak każdą inną wartość w laboratorium.
Kod do tej lekcji: index.js
1// Programowanie funkcyjne - Wprowadzenie
2console.log("=== Park Jurajski - Laboratorium Czystych Funkcji ===\n");
3
4// PRZYKLAD 1: Czysta vs nieczysta funkcja
5
6// CZYSTA FUNKCJA - zawsze ten sam wynik, brak efektow ubocznych
7function calculateDangerLevel(weight, speed, aggression) {
8 return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
9}
10
11console.log("--- Czyste funkcje ---");
12console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
13console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
14console.log("Identyczne wyniki! Czysta funkcja jest deterministyczna.\n");
15
16// NIECZYSTA FUNKCJA - efekty uboczne
17let totalRegistered = 0;
18function registerDinosaurImpure(name) {
19 totalRegistered++; // Efekt uboczny!
20 return { name, id: totalRegistered };
21}
22
23console.log("--- Nieczysty przyklad ---");
24console.log("Rex:", registerDinosaurImpure("Rex"));
25console.log("Rex:", registerDinosaurImpure("Rex"));
26console.log("Rozne wyniki! Funkcja zalezy od stanu zewnetrznego.\n");
27
28// PRZYKLAD 2: Niemutowalnosc
29console.log("--- Niemutowalnosc ---");
30const dinosaur = { name: "Rex", health: 100 };
31const injuredDino = { ...dinosaur, health: 80 };
32
33console.log("Oryginal:", dinosaur);
34console.log("Nowy obiekt:", injuredDino);
35console.log("Oryginal niezmieniony:", dinosaur.health === 100);
36
37// PRZYKLAD 3: Funkcje jako wartosci
38console.log("\n--- Funkcje jako wartosci ---");
39const classify = (dino) => dino.diet === "carnivore" ? "dangerous" : "safe";
40
41const dinos = [
42 { name: "Rex", diet: "carnivore" },
43 { name: "Brachio", diet: "herbivore" },
44 { name: "Raptor", diet: "carnivore" },
45];
46
47const dangerous = dinos.filter((d) => classify(d) === "dangerous");
48console.log("Niebezpieczne:", dangerous.map((d) => d.name));
49
50// PRZYKLAD 4: Funkcja zwracajaca funkcje
51function createWeightFilter(minWeight) {
52 return (dino) => dino.weight >= minWeight;
53}
54
55const isHeavy = createWeightFilter(5000);
56console.log("\nRex (8000kg) jest ciezki?", isHeavy({ weight: 8000 }));
57console.log("Raptor (150kg) jest ciezki?", isHeavy({ weight: 150 }));Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Która z poniższych funkcji jest czysta (pure function)?