Kurryzacja i Aplikacja Częściowa w JS

12/03/2023

Rating: 4.44 (5518 votes)

W świecie programowania, gdzie złożoność kodu rośnie z każdym projektem, poszukiwanie elegancji, modułowości i łatwości w utrzymaniu staje się priorytetem. Programowanie funkcyjne oferuje zestaw narzędzi i paradygmatów, które pomagają osiągnąć te cele. Dwie z najbardziej fascynujących i potężnych technik w tym arsenale to kurryzacja (currying) i aplikacja częściowa (partial application). Choć często mylone, każda z nich ma swoją unikalną rolę i ogromny potencjał do usprawnienia Twojego kodu JavaScript.

What is currying in Computer Science?
At one point in the series, he mentioned currying. Both computer science and mathematics agree on the definition: Currying turns multi-argument functions into unary (single argument) functions. Curried functions take many arguments one at a time. So if you have Properly currying greet gives you

Zacznijmy od zrozumienia, czym jest kurryzacja – koncepcja, która może wydawać się skomplikowana na pierwszy rzut oka, ale w rzeczywistości jest niezwykle logiczna i użyteczna.

Co to jest Kurryzacja?

Kurryzacja to technika transformacji funkcji, która przyjmuje wiele argumentów, w sekwencję funkcji, z których każda przyjmuje tylko jeden argument. Innymi słowy, funkcja wieloargumentowa jest rozkładana na serię funkcji jednoargumentowych (unarnych). Nazwa pochodzi od logika Haskella Curry'ego, choć koncepcja była już obecna w pracach Mosesa Schönfinkela.

Wyobraź sobie prostą funkcję powitania, która przyjmuje trzy argumenty: pozdrowienie, imię i nazwisko:

const powitaj = (pozdrowienie, imie, nazwisko) => `${pozdrowienie}, ${imie} ${nazwisko}`; powitaj('Witaj', 'Jan', 'Kowalski'); // Wynik: "Witaj, Jan Kowalski"

Poprawnie skurryzowana wersja tej funkcji wyglądałaby tak:

const skurryzowanePowitaj = curry(powitaj); skurryzowanePowitaj('Witaj')('Jan')('Kowalski');

Jak widać, funkcja, która pierwotnie przyjmowała trzy argumenty jednocześnie, została przekształcona w trzy funkcje jednoargumentowe. Kiedy dostarczasz jeden parametr, pojawia się nowa funkcja, która oczekuje kolejnego. To nie tylko zmienia sposób wywoływania funkcji, ale otwiera drzwi do niezwykłej elastyczności.

Zalety Kurryzacji

Kurryzacja oferuje kilka kluczowych korzyści, które mogą znacząco poprawić jakość Twojego kodu:

  • Reużywalność (Ponowne wykorzystanie kodu): Skurryzowane funkcje umożliwiają tworzenie nowych, wyspecjalizowanych funkcji poprzez "zamrażanie" (ustalanie) niektórych argumentów. To zwiększa rezywalność, pozwalając na łatwe tworzenie wariantów funkcji bez duplikowania logiki.
// Oryginalna funkcja function dodaj(a, b) { return a + b; } // Skurryzowana wersja (uproszczona) function skurryzujDodaj(a) { return function(b) { return a + b; }; } const dodaj5 = skurryzujDodaj(5); // Tworzy wyspecjalizowaną funkcję dodającą 5 do dowolnej liczby console.log(dodaj5(3)); // Wynik: 8 console.log(dodaj5(10)); // Wynik: 15

W tym przykładzie, skurryzujDodaj pozwala nam stworzyć dodaj5, która zawsze dodaje 5, niezależnie od drugiego argumentu. To klasyczny przykład reużywalności.

  • Kompozycja Funkcji: Kurryzacja ułatwia kompozycję funkcji. Kompozycja funkcji to proces łączenia dwóch lub więcej funkcji w celu stworzenia nowej funkcji, która wykonuje sekwencję operacji.
// Funkcje do kompozycji function pomnoz(a, b) { return a * b; } function dodajJeden(x) { return x + 1; } // Skurryzowane wersje (uproszczone) const skurryzujPomnoz = a => b => a * b; const skurryzujDodajJeden = x => x + 1; // Kompozycja const dodajJedenPotemPomnozPrzez5 = x => skurryzujPomnoz(5)(skurryzujDodajJeden(x)); console.log(dodajJedenPotemPomnozPrzez5(3)); // Wynik: 20 (5 * (3 + 1))

Dzięki kurryzacji, łączenie pomnoz i dodajJeden staje się intuicyjne, tworząc nową funkcję, która wyraźnie opisuje sekwencję operacji.

Czym różni się Kurryzacja od Aplikacji Częściowej?

Choć ściśle powiązane i często używane zamiennie, kurryzacja i aplikacja częściowa nie są tym samym. Zrozumienie ich subtelnych różnic jest kluczowe dla efektywnego programowania funkcyjnego.

What is currying in Computer Science?
At one point in the series, he mentioned currying. Both computer science and mathematics agree on the definition: Currying turns multi-argument functions into unary (single argument) functions. Curried functions take many arguments one at a time. So if you have Properly currying greet gives you

Aplikacja częściowa to technika tworzenia nowej funkcji poprzez wypełnianie niektórych argumentów oryginalnej funkcji z góry. Niekoniecznie prowadzi to do funkcji jednoargumentowych, jak w przypadku kurryzacji, ale raczej do funkcji, która oczekuje pozostałych argumentów.

Rozważmy funkcję formatującą wiadomość:

function formatujWiadomosc(pozdrowienie, imie) { return `${pozdrowienie}, ${imie}!`; }

Możemy stworzyć funkcję częściowo zaaplikowaną:

function czesciowa(fn, ...ustawioneArgumenty) { return function(...pozostaleArgumenty) { return fn(...ustawioneArgumenty, ...pozostaleArgumenty); }; } // Stwórz funkcję, która zawsze wita "Cześć" const witajCzescia = czesciowa(formatujWiadomosc, "Cześć"); console.log(witajCzescia("Alicja")); // Wynik: Cześć, Alicja! console.log(witajCzescia("Bob")); // Wynik: Cześć, Bob!

Tutaj witajCzescia jest funkcją, która zawsze używa "Cześć" jako pozdrowienia. Przyjmuje jeden argument (imię), mimo że oryginalna funkcja przyjmowała dwa. To jest aplikacja częściowa.

Kluczowe Różnice

CechaKurryzacjaAplikacja Częściowa
Cel transformacjiZawsze zwraca serię funkcji jednoargumentowych.Zwraca funkcję, która przyjmuje pozostałe, niewypełnione argumenty (może być ich wiele).
Liczba argumentówFunkcja wynikowa zawsze oczekuje dokładnie jednego argumentu na raz.Funkcja wynikowa może oczekiwać dowolnej liczby pozostałych argumentów.
ZastosowanieIdealna do kompozycji funkcji i tworzenia "łańcuchów" wywołań.Idealna do tworzenia wyspecjalizowanych wersji funkcji poprzez wstępne wypełnianie argumentów.

Zalety Aplikacji Częściowej

  • Uproszczenie: Tworzenie prostszych funkcji ze złożonych. Jeśli masz funkcję z wieloma argumentami, z których niektóre często mają te same wartości, aplikacja częściowa pozwala stworzyć uproszczone wersje tej funkcji.
  • function obliczCeneKoncowa(cena, rabat, podatek) { return cena - (cena * rabat) + (cena * podatek); } function zastosujRabatIPodatek(rabat, podatek) { return function(cena) { return obliczCeneKoncowa(cena, rabat, podatek); }; } const zastosujStandardoweStawki = zastosujRabatIPodatek(0.1, 0.08); // Rabat 10%, podatek 8% console.log(zastosujStandardoweStawki(100)); // Wynik: 98 (100 - 10 + 8)
  • Ponowne Wykorzystanie Kodu: Ponowne wykorzystanie wspólnej logiki funkcji z różnymi wstępnie ustawionymi argumentami.
  • function logujWiadomosc(poziom, wiadomosc) { console.log(`[${poziom}] ${wiadomosc}`); } function stworzLogger(poziom) { return function(wiadomosc) { logujWiadomosc(poziom, wiadomosc); }; } const infoLogger = stworzLogger('INFO'); const errorLogger = stworzLogger('ERROR'); infoLogger('To jest wiadomość informacyjna.'); errorLogger('To jest wiadomość o błędzie.');
  • Lepsza Czytelność: Ułatwia czytanie kodu poprzez rozbijanie złożonych funkcji na mniejsze, bardziej zrozumiałe części.
  • function formatujDate(data, format) { const options = { year: 'numeric', month: '2-digit', day: '2-digit' }; if (format === 'US') { options.month = 'long'; } else if (format === 'EU') { options.day = 'numeric'; options.month = 'numeric'; } return new Date(data).toLocaleDateString(undefined, options); } function stworzFormatDaty(format) { return function(data) { return formatujDate(data, format); }; } const formatDatyUS = stworzFormatDaty('US'); const formatDatyEU = stworzFormatDaty('EU'); console.log(formatDatyUS('2023-05-15')); // Wynik: May 15, 2023 (lub podobnie, zależnie od locale) console.log(formatDatyEU('2023-05-15')); // Wynik: 15.05.2023 (lub podobnie, zależnie od locale)

Głębsze Zanurzenie: Jak to działa pod maską?

Magia zarówno kurryzacji, jak i elastycznych implementacji aplikacji częściowej, polega na dwóch kluczowych cechach JavaScriptu: domknięciach (closures) i właściwości .length funkcji.

Wspomniana wcześniej implementacja funkcji curry, elastyczna jak ta z biblioteki Ramda, pozwala na wywoływanie skurryzowanej funkcji zarówno z pojedynczymi argumentami, jak i z ich grupami (np. curriedGreet('Hello', 'Bruce')('Wayne')). Jej serce bije dzięki rekurencji i właściwości .length funkcji. W JavaScript, właściwość .length funkcji informuje, ile argumentów funkcja oczekuje (ile parametrów zostało zadeklarowanych w jej sygnaturze, przed rest parametrem lub domyślnymi wartościami).

Rozważmy uproszczoną wersję elastycznej funkcji curry:

const curry = (oryginalnaFunkcja, poczatkoweParametry = []) => { return (...nastepneParametry) => { const wszystkieParametry = [...poczatkoweParametry, ...nastepneParametry]; if (wszystkieParametry.length >= oryginalnaFunkcja.length) { // Jeśli zebrano wystarczająco argumentów, wywołaj oryginalną funkcję return oryginalnaFunkcja(...wszystkieParametry); } else { // W przeciwnym razie, zwróć nową funkcję, która oczekuje więcej argumentów // i jest rekurencyjnie wywołaniem curry z już zebranymi argumentami return curry(oryginalnaFunkcja, wszystkieParametry); } }; };

Kiedy wywołujemy curry(greet), funkcja curry zwraca nową funkcję. Ta nowa funkcja to "dziecko" funkcji curry i dzięki mechanizmowi domknięć ma dostęp do oryginalnaFunkcja (czyli greet) i poczatkoweParametry (początkowo pusta tablica) z zakresu swojego rodzica. Nawet gdy curry zakończy swoje pierwsze wykonanie, te zmienne nie są usuwane, ponieważ "dziecko" wciąż ich potrzebuje.

Gdy wywołamy curriedGreet('Hello'), argument 'Hello' jest przechwytywany przez składnię rest (...nastepneParametry) jako tablica ['Hello']. Wewnątrz funkcji sprawdzamy, czy łączna liczba zebranych argumentów (['Hello']) jest równa lub większa niż oczekiwana liczba argumentów przez greet (czyli 3, bo greet ma pozdrowienie, imie, nazwisko). Ponieważ 1 < 3, warunek jest fałszywy, i funkcja curry jest wywoływana ponownie, ale tym razem z greet i ['Hello'] jako poczatkoweParametry. To jest właśnie rekurencja – funkcja wywołuje sama siebie, kontynuując zbieranie argumentów.

Ten proces powtarza się, dopóki wszystkie wymagane argumenty nie zostaną zebrane. Gdy wszystkieParametry.length osiągnie lub przekroczy oryginalnaFunkcja.length, oryginalna funkcja jest w końcu wywoływana z zebranymi argumentami, a proces kurryzacji się kończy, zwracając ostateczny wynik.

How to compose a function using currying?
Function composition is the process of combining two or more functions to produce a new function. In this example, we compose multiply and addOne functions using currying. By currying these functions, we can easily create a new function addOneThenMultiplyBy5, which first adds 1 to the input (x) and then multiplies the result by 5.

Ta elastyczność sprawia, że implementacje kurryzacji są niezwykle potężne, pozwalając na swobodne dostarczanie argumentów pojedynczo lub w grupach, jednocześnie zachowując logikę oczekiwania na wszystkie niezbędne dane.

Najlepsze Praktyki Stosowania Kurryzacji i Aplikacji Częściowej

Aby w pełni wykorzystać potencjał kurryzacji i aplikacji częściowej, warto przestrzegać kilku najlepszych praktyk:

  • Zachowaj Czyste Funkcje: Upewnij się, że skurryzowane i częściowo zaaplikowane funkcje pozostają czyste funkcje, tzn. nie mają efektów ubocznych (nie modyfikują stanu zewnętrznego ani nie wywołują operacji I/O). Czyste funkcje są łatwiejsze do zrozumienia, testowania i komponowania.
  • Stosuj Umiarkowanie: Używaj kurryzacji i aplikacji częściowej, gdy naturalnie pasują do rozwiązywanego problemu. Nadmierne stosowanie tych technik może sprawić, że kod będzie trudniejszy do zrozumienia, jeśli nie zostanie użyty rozsądnie.
  • Wykorzystaj Kompozycję Funkcji: Łącz skurryzowane funkcje, aby tworzyć bardziej złożoną funkcjonalność. Kurryzacja doskonale współgra z kompozycją funkcji, prowadząc do bardziej modułowego i czytelnego kodu.
  • Dokumentacja i Nazewnictwo: Odpowiednio dokumentuj skurryzowane i częściowo zaaplikowane funkcje, aby wskazać ich oczekiwane zastosowanie. Używaj jasnych i opisowych nazw funkcji, aby przekazać ich cel.
  • Integracja z Metodami Tablicowymi: Kurryzacja może być skutecznie łączona z metodami tablicowymi, takimi jak map, filter i reduce, co prowadzi do zwięzłego i ekspresyjnego kodu.
  • const skurryzujPomnoz = a => b => a * b; const liczby = [1, 2, 3, 4, 5]; const pomnozPrzezDwa = skurryzujPomnoz(2); // Częściowo zaaplikowana/skurryzowana funkcja const podwojoneLiczby = liczby.map(pomnozPrzezDwa); console.log(podwojoneLiczby); // Wynik: [2, 4, 6, 8, 10]
  • Rozważ Użycie Bibliotek: Jeśli często korzystasz z kurryzacji i aplikacji częściowej, rozważ użycie bibliotek takich jak Lodash (np. _.curry i _.partial) lub Ramda, które dostarczają gotowe, solidne implementacje tych funkcji.

Często Zadawane Pytania (FAQ)

Czy kurryzacja i aplikacja częściowa są tym samym?

Nie. Kurryzacja transformuje funkcję tak, aby każda kolejna funkcja przyjmowała dokładnie jeden argument, dopóki wszystkie nie zostaną zebrane. Aplikacja częściowa tworzy nową funkcję poprzez wstępne wypełnienie *niektórych* argumentów oryginalnej funkcji; nowa funkcja może nadal przyjmować wiele argumentów.

Kiedy powinienem używać kurryzacji?

Kurryzacja jest szczególnie przydatna, gdy chcesz tworzyć funkcje, które są łatwo komponowalne, lub gdy potrzebujesz tworzyć wiele wyspecjalizowanych wersji funkcji, zmieniając tylko jeden argument na raz. Jest również świetna do technik programowania, gdzie argumenty są dostarczane stopniowo.

Czy ma to zastosowanie tylko w JavaScript?

Absolutnie nie! Koncepcje kurryzacji i aplikacji częściowej są fundamentalnymi elementami programowania funkcyjnego i są obecne w wielu językach programowania obsługujących ten paradygmat, takich jak Haskell, Scala, F#, Python (częściowo), a także w bibliotekach funkcyjnych dla C#, Java, itp.

Czy używanie kurryzacji lub aplikacji częściowej wpływa na wydajność?

W większości współczesnych środowisk JavaScript, narzut związany z tworzeniem dodatkowych domknięć i funkcji jest minimalny i zazwyczaj pomijalny dla większości aplikacji. Dla operacji o krytycznym znaczeniu dla wydajności zawsze warto przeprowadzić profilowanie, ale w typowych scenariuszach korzyści z czystości, modułowości i czytelności kodu znacznie przewyższają potencjalne drobne spadki wydajności.

Podsumowanie

Kurryzacja i aplikacja częściowa to potężne techniki programowania funkcyjnego, które mogą znacząco poprawić modułowość, reużywalność i czytelność Twojego kodu JavaScript. Kurryzacja przekształca funkcje wieloargumentowe w serię funkcji jednoargumentowych, umożliwiając bardziej elastyczny i modułowy kod. Aplikacja częściowa pozwala na wstępne wypełnienie argumentów funkcji, ułatwiając ponowne wykorzystanie kodu i upraszczając złożone funkcje. Poprzez wykorzystanie tych koncepcji programowania funkcyjnego, programiści mogą pisać czystszy, bardziej zwięzły i łatwiejszy w utrzymaniu kod JavaScript.

Mam nadzieję, że ten artykuł rozjaśnił te koncepcje i zainspirował Cię do eksperymentowania z nimi w Twoich projektach. Do zobaczenia w świecie funkcjonalnych transformacji!

Zainteresował Cię artykuł Kurryzacja i Aplikacja Częściowa w JS? Zajrzyj też do kategorii Kulinaria, znajdziesz tam więcej podobnych treści!

Go up