Wprowadzenie do programowania funkcjonalnego: paradygmaty JavaScript
Opublikowany: 2022-03-11Programowanie funkcjonalne to paradygmat budowania programów komputerowych przy użyciu wyrażeń i funkcji bez mutowania stanu i danych.
Respektując te ograniczenia, programowanie funkcjonalne ma na celu napisanie kodu, który jest jaśniejszy do zrozumienia i bardziej odporny na błędy. Osiąga się to poprzez unikanie używania instrukcji kontroli przepływu ( for
, while
, break
, continue
, goto
), które utrudniają śledzenie kodu. Ponadto programowanie funkcjonalne wymaga od nas pisania czystych, deterministycznych funkcji, które są mniej podatne na błędy.
W tym artykule omówimy programowanie funkcjonalne przy użyciu JavaScript. Przyjrzymy się również różnym metodom i funkcjom JavaScript, które to umożliwiają. Na koniec przyjrzymy się różnym koncepcjom związanym z programowaniem funkcjonalnym i zobaczymy, dlaczego są one tak potężne.
Zanim jednak przejdziemy do programowania funkcjonalnego, należy zrozumieć różnicę między czystymi i nieczystymi funkcjami.
Czyste kontra nieczyste funkcje
Czyste funkcje pobierają trochę danych wejściowych i dają stałe wyjście. Ponadto nie powodują skutków ubocznych w świecie zewnętrznym.
const add = (a, b) => a + b;
Tutaj add
to czysta funkcja. Dzieje się tak, ponieważ dla ustalonej wartości a
i b
dane wyjściowe będą zawsze takie same.
const SECRET = 42; const getId = (a) => SECRET * a;
getId
nie jest czystą funkcją. Powodem jest to, że używa globalnej zmiennej SECRET
do obliczania danych wyjściowych. Jeśli SECRET
miałby się zmienić, funkcja getId
zwróci inną wartość dla tego samego wejścia. Nie jest to zatem czysta funkcja.
let id_count = 0; const getId = () => ++id_count;
Jest to również funkcja nieczysta i to również z kilku powodów — (1) używa zmiennej nielokalnej do obliczania swoich danych wyjściowych i (2) tworzy efekt uboczny w świecie zewnętrznym, modyfikując zmienną w tym świat.
Może to być kłopotliwe, gdybyśmy musieli debugować ten kod.
Jaka jest aktualna wartość id_count
? Jakie inne funkcje modyfikują id_count
? Czy istnieją inne funkcje oparte na id_count
?
Z tych powodów w programowaniu funkcjonalnym używamy wyłącznie czystych funkcji.
Inną zaletą czystych funkcji jest to, że można je zrównoleglać i zapamiętywać. Spójrz na dwie poprzednie funkcje. Nie da się ich zrównoleglić ani zapamiętać. Pomaga to w tworzeniu wydajnego kodu.
Zasady programowania funkcjonalnego
Do tej pory nauczyliśmy się, że programowanie funkcjonalne jest zależne od kilku zasad. Są one następujące.
- Nie mutuj danych
- Używaj czystych funkcji: stałe wyjście dla stałych wejść i brak efektów ubocznych
- Używaj wyrażeń i deklaracji
Kiedy spełnimy te warunki, możemy powiedzieć, że nasz kod działa.
Programowanie funkcjonalne w JavaScript
JavaScript ma już kilka funkcji, które umożliwiają programowanie funkcjonalne. Przykład: String.prototype.slice, Array.protoype.filter, Array.prototype.join.
Z drugiej strony, Array.prototype.forEach, Array.prototype.push są nieczystymi funkcjami.
Można argumentować, że Array.prototype.forEach
nie jest nieczystą funkcją z założenia, ale zastanów się nad tym — nie można z nią zrobić niczego poza mutowaniem danych nielokalnych lub wywoływaniem skutków ubocznych. Tak więc można to umieścić w kategorii funkcji nieczystych.
Ponadto JavaScript ma deklarację const, która jest idealna do programowania funkcjonalnego, ponieważ nie będziemy mutować żadnych danych.
Czyste funkcje w JavaScript
Przyjrzyjmy się niektórym czystym funkcjom (metodom) dostarczanym przez JavaScript.
Filtr
Jak sama nazwa wskazuje, filtruje to tablicę.
array.filter(condition);
Warunkiem jest tutaj funkcja, która pobiera każdy element tablicy i powinna zdecydować, czy zachować ten element, czy nie, i zwrócić dla tego prawdziwą wartość logiczną.
const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]
Zauważ, że filterEven
jest czystą funkcją. Gdyby był nieczysty, spowodowałoby to nieczystość całego wywołania filtra.
Mapa
map
mapuje każdy element tablicy do funkcji i tworzy nową tablicę na podstawie wartości zwracanych przez wywołania funkcji.
array.map(mapper)
mapper
to funkcja, która pobiera element tablicy jako dane wejściowe i zwraca dane wyjściowe.
const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]
Redukować
reduce
zmniejsza tablicę do pojedynczej wartości.
array.reduce(reducer);
reducer
to funkcja, która pobiera skumulowaną wartość i następny element tablicy i zwraca nową wartość. Nazywa się to tak dla wszystkich wartości w tablicy, jedna po drugiej.
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6
Concat
concat
dodaje nowe elementy do istniejącej tablicy, aby utworzyć nową tablicę. Różni się od push()
w tym sensie, że push()
mutuje dane, co czyni je nieczystymi.

[1, 2].concat([3, 4]) // [1, 2, 3, 4]
Możesz również zrobić to samo za pomocą operatora rozsunięcia.
[1, 2, ...[3, 4]]
Obiekt.przypisz
Object.assign
kopiuje wartości z podanego obiektu do nowego obiektu. Ponieważ programowanie funkcjonalne opiera się na niezmiennych danych, używamy go do tworzenia nowych obiektów na podstawie istniejących obiektów.
const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2
Wraz z pojawieniem się ES6 można to również zrobić za pomocą operatora rozsunięcia.
const newObj = {...obj};
Tworzenie własnej czystej funkcji
Możemy również stworzyć naszą czystą funkcję. Zróbmy jedną dla zduplikowania łańcucha n
razy.
const duplicate = (str, n) => n < 1 ? '' : str + duplicate(str, n-1);
Ta funkcja powiela ciąg n
razy i zwraca nowy ciąg.
duplicate('hooray!', 3) // hooray!hooray!hooray!
Funkcje wyższego rzędu
Funkcje wyższego rzędu to funkcje, które akceptują funkcję jako argument i zwracają funkcję. Często są używane w celu zwiększenia funkcjonalności funkcji.
const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };
W powyższym przykładzie tworzymy funkcję wyższego rzędu withLog
, która przyjmuje funkcję i zwraca funkcję, która rejestruje komunikat przed uruchomieniem opakowanej funkcji.
const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7
withLog
HOF może być używany również z innymi funkcjami i działa bez żadnych konfliktów ani pisania dodatkowego kodu. Na tym polega piękno HOF.
const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!
Można to również nazwać bez definiowania funkcji łączącej.
withLog(hype)('Sale'); // calling hype // Sale!!!
Curry
Currying oznacza rozbicie funkcji, która przyjmuje wiele argumentów na jeden lub wiele poziomów funkcji wyższego rzędu.
Weźmy funkcję add
.
const add = (a, b) => a + b;
Kiedy mamy to zrobić, przepisujemy to, rozkładając argumenty na wiele poziomów w następujący sposób.
const add = a => { return b => { return a + b; }; }; add(3)(4); // 7
Zaletą curryingu jest zapamiętywanie. Możemy teraz zapamiętać pewne argumenty w wywołaniu funkcji, aby można je było później ponownie wykorzystać bez powielania i ponownego obliczania.
// assume getOffsetNumer() call is expensive const addOffset = add(getOffsetNumber()); addOffset(4); // 4 + getOffsetNumber() addOffset(6);
Jest to z pewnością lepsze niż używanie obu argumentów wszędzie.
// (X) DON"T DO THIS add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());
Możemy również przeformatować naszą funkcję curried, aby wyglądała zwięźle. Dzieje się tak, ponieważ każdy poziom wywołania funkcji currying to jednowierszowa instrukcja powrotu. Dlatego możemy użyć funkcji strzałek w ES6, aby dokonać refaktoryzacji w następujący sposób.
const add = a => b => a + b;
Kompozycja
W matematyce kompozycja jest definiowana jako przekazywanie danych wyjściowych jednej funkcji do danych wejściowych innej, aby utworzyć połączone dane wyjściowe. To samo jest możliwe w programowaniu funkcjonalnym, ponieważ używamy czystych funkcji.
Aby pokazać przykład, stwórzmy kilka funkcji.
Pierwsza funkcja to range, która pobiera liczbę początkową a
i końcową b
i tworzy tablicę złożoną z liczb od a
do b
.
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];
Następnie mamy funkcję mnożenia, która pobiera tablicę i mnoży wszystkie zawarte w niej liczby.
const multiply = arr => arr.reduce((p, a) => p * a);
Użyjemy tych funkcji razem do obliczenia silni.
const factorial = n => multiply(range(1, n)); factorial(5); // 120 factorial(6); // 720
Powyższa funkcja do obliczania silni jest podobna do f(x) = g(h(x))
, demonstrując w ten sposób właściwość kompozycji.
Słowa końcowe
Przeszliśmy przez czyste i nieczyste funkcje, programowanie funkcjonalne, nowe funkcje JavaScript, które w tym pomagają, oraz kilka kluczowych pojęć w programowaniu funkcjonalnym.
Mamy nadzieję, że ten artykuł wzbudzi Twoje zainteresowanie programowaniem funkcjonalnym i prawdopodobnie zmotywuje Cię do wypróbowania go w swoim kodzie. Jesteśmy przekonani, że będzie to nauka i kamień milowy w Twojej podróży do tworzenia oprogramowania.
Programowanie funkcjonalne to dobrze zbadany i solidny paradygmat pisania programów komputerowych. Wraz z wprowadzeniem ES6 JavaScript pozwala na znacznie lepsze wrażenia z programowania funkcjonalnego niż kiedykolwiek wcześniej.