Wprowadzenie do programowania funkcjonalnego: paradygmaty JavaScript

Opublikowany: 2022-03-11

Programowanie 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.

getId to nieczysta ilustracja

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.

  1. Nie mutuj danych
  2. Używaj czystych funkcji: stałe wyjście dla stałych wejść i brak efektów ubocznych
  3. 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 

zmniejszyć ilustrację połączenia

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.