Kurs JavaScript i TypeScript · Moduł 12: Programowanie funkcyjne
Currying i częściowa aplikacja
W tej lekcji5
Wyobraź sobie system karmienia dinozaurów w parku. Każdy gatunek ma swoje wymagania - rodzaj pożywienia, porcję i porę karmienia. Zamiast za każdym razem podawać wszystkie parametry, możemy przygotować funkcje "wstępnie skonfigurowane" dla danego gatunku. To właśnie robi currying - przekształca funkcję wieloargumentową w sekwencję funkcji jednoargumentowych.
Jest też drugi, bardziej techniczny problem. pipe z poprzedniej lekcji przekazuje każdemu etapowi dokładnie jedną wartość, więc funkcja wymagająca gatunku, pokarmu i porcji po prostu do niego nie pasuje. Potrzebujemy sposobu, żeby część argumentów ustawić wcześniej, a na końcu zostawić funkcję jednoargumentową.
Czym jest currying?
Currying to technika przekształcania funkcji przyjmującej wiele argumentów w sekwencję funkcji, z których każda przyjmuje dokładnie jeden argument.
Zacznijmy od zwykłej funkcji z trzema parametrami i jej wersji curried zapisanej klasycznie, przez zagnieżdżone function:
1// Zwykła funkcja z 3 argumentami
2function feedDinosaur(species, food, portion) {
3 return species + ' dostaje ' + portion + 'kg ' + food;
4}
5feedDinosaur('T-Rex', 'meat', 50);
6
7// Wersja curried - sekwencja funkcji jednoargumentowych
8function feedDinosaurCurried(species) {
9 return function(food) {
10 return function(portion) {
11 return species + ' dostaje ' + portion + 'kg ' + food;
12 };
13 };
14}
15feedDinosaurCurried('T-Rex')('meat')(50);Oba wywołania zwracają 'T-Rex dostaje 50kg meat'. Wywołanie feedDinosaurCurried('T-Rex') nikogo jeszcze nie karmi, tylko zwraca funkcję czekającą na pokarm, a dopiero trzecia para nawiasów uruchamia właściwą logikę. Wewnętrzne funkcje pamiętają species i food dzięki domknięciom, którym przyjrzymy się w następnej lekcji.
Funkcje strzałkowe pozwalają zapisać to samo znacznie krócej, bo każda strzałka to kolejny etap:
1// To samo ze strzałkami (bardziej zwięźle)
2const feedCurried = (species) => (food) => (portion) =>
3 species + ' dostaje ' + portion + 'kg ' + food;Czytaj to od lewej: funkcja od gatunku zwraca funkcję od pokarmu, a ta zwraca funkcję od porcji. Zmienił się tylko zapis, zachowanie jest identyczne jak w wersji z function.
Praktyczne zastosowanie curryingu
Prawdziwa siła curryingu tkwi w tworzeniu wyspecjalizowanych funkcji. Tym razem ostatni etap zwraca obiekt karmienia ze znacznikiem czasu:
1const feedCurried = (species) => (food) => (portion) =>
2 ({ species, food, portion, timestamp: Date.now() });
3
4// Specjalizowane funkcje dla gatunków
5const feedRex = feedCurried('T-Rex');
6const feedRexMeat = feedRex('meat');
7
8// Użycie wyspecjalizowanych funkcji
9feedRexMeat(50); // { species: 'T-Rex', food: 'meat', portion: 50, ... }
10feedRexMeat(30); // { species: 'T-Rex', food: 'meat', portion: 30, ... }
11
12// Można też dla innego gatunku
13const feedBrachio = feedCurried('Brachiosaurus');
14const feedBrachioPlants = feedBrachio('plants');
15feedBrachioPlants(200); // { species: 'Brachiosaurus', food: 'plants', portion: 200, ... }feedRex i feedRexMeat to zwykłe funkcje, które możesz przechowywać, przekazywać i wywoływać wiele razy, a feedCurried pozostaje ogólna i bez przeszkód obsłuży Brachiozaura. Zapis { species, food, portion } to skrót od { species: species, ... }. Uwaga techniczna: Date.now() sprawia, że ostatni etap nie jest czysty, bo dwa wywołania z tą samą porcją dadzą różne znaczniki czasu. Wersja bez czasu, const feed = (species) => (food) => (portion) => ({ species, food, portion }), jest już czysta.
Uniwersalna funkcja curry
Możemy napisać uniwersalną funkcję curry, która automatycznie przekształca dowolną funkcję. Kluczem jest właściwość fn.length, czyli liczba parametrów zadeklarowanych przez funkcję (bez parametru resztowego i bez parametrów od pierwszego z wartością domyślną):
1function curry(fn) {
2 return function curried(...args) {
3 if (args.length >= fn.length) {
4 return fn(...args);
5 }
6 return (...moreArgs) => curried(...args, ...moreArgs);
7 };
8}curried zbiera argumenty. Gdy jest ich już co najmniej tyle, ile wynosi fn.length, wywołuje oryginał, a w przeciwnym razie zwraca funkcję, która dołoży kolejne. Sprawdźmy to na funkcji budującej wybieg:
1// Użycie
2function createEnclosure(zone, type, capacity, voltage) {
3 return { zone, type, capacity, voltage };
4}
5
6const curriedCreate = curry(createEnclosure);
7
8// Można wywołać na różne sposoby:
9curriedCreate('A')('carnivore')(5)(10000);
10curriedCreate('A', 'carnivore')(5, 10000);
11curriedCreate('A', 'carnivore', 5)(10000);
12curriedCreate('A', 'carnivore', 5, 10000);
13// Wszystkie wywołania dają ten sam wynikKażde wywołanie zwraca { zone: 'A', type: 'carnivore', capacity: 5, voltage: 10000 }. Ściśle biorąc, wersja przyjmująca kilka argumentów naraz łączy currying z częściową aplikacją. Tak działają też funkcje curry z bibliotek Ramda i lodash. Pułapka: funkcja zadeklarowana jako (...args) => ... ma length równe 0, więc curry wywoła ją od razu.
Częściowa aplikacja (Partial Application)
Częściowa aplikacja to pokrewna technika - "zamraża" część argumentów funkcji, tworząc nową funkcję z mniejszą liczbą parametrów.
partial przyjmuje funkcję i dowolną liczbę argumentów startowych (presetArgs), a zwraca funkcję czekającą na resztę (laterArgs). Operator spread skleja obie listy w jedno wywołanie:
1function partial(fn, ...presetArgs) {
2 return (...laterArgs) => fn(...presetArgs, ...laterArgs);
3}Zastosujmy ją do funkcji dziennika incydentów, z której korzysta cała ochrona parku, i zbudujmy z niej kilka wyspecjalizowanych loggerów:
1// Funkcja logowania incydentów
2function logIncident(severity, zone, message) {
3 return '[' + severity + '] Zone ' + zone + ': ' + message;
4}
5
6// Wyspecjalizowane loggery
7const logCritical = partial(logIncident, 'CRITICAL');
8const logWarning = partial(logIncident, 'WARNING');
9const logCriticalZoneA = partial(logIncident, 'CRITICAL', 'A');
10
11logCritical('B', 'Fence breach detected');
12// '[CRITICAL] Zone B: Fence breach detected'
13
14logCriticalZoneA('T-Rex escaped!');
15// '[CRITICAL] Zone A: T-Rex escaped!'W logCritical zamrożony jest jeden argument, a w logCriticalZoneA od razu dwa. Pierwotna logIncident się nie zmieniła i nadal przyjmuje trzy argumenty. Ten sam efekt da wbudowane bind: logIncident.bind(null, 'CRITICAL').
Currying vs częściowa aplikacja
Choć obie techniki tworzą wyspecjalizowane funkcje, różnią się sposobem działania:
- Currying: zawsze tworzy łańcuch funkcji jednoargumentowych
- Częściowa aplikacja: "zamraża" dowolną liczbę argumentów naraz
Specjalizacja przez currying ma zawsze te same etapy: definiujesz ogólną funkcję curried, podajesz pierwszy argument i dostajesz funkcję częściowo zastosowaną, kolejne argumenty dalej ją zawężają, a ostatni zwraca wynik. Moja rada: funkcje do pipeline'ów pisz w kolejności "konfiguracja najpierw, dane na końcu", na przykład filterBy(key)(value)(arr). Po podaniu konfiguracji zostaje funkcja jednoargumentowa gotowa do pipe, a do jednorazowego zamrożenia kilku argumentów istniejącej funkcji wystarczy partial. W projekcie końcowym właśnie tak zbudujesz narzędzia do filtrowania odczytów.
Pamiętaj: currying to stacja karmienia konfigurowana krok po kroku - gatunek, pokarm, porcja - a każde ustawienie daje Ci nową, gotową do pracy maszynę.
Kod do tej lekcji: index.js
1// Currying i czesciowa aplikacja
2console.log("=== Park Jurajski - Etapowe Przetwarzanie Gatunkow ===\n");
3
4// --- PRZYKLAD 1: Podstawowy currying ---
5console.log("--- Currying: System karmienia ---");
6
7const feedDinosaur = (species) => (food) => (portion) =>
8 `${species} dostaje ${portion}kg ${food}`;
9
10// Specjalizowane funkcje
11const feedRex = feedDinosaur("T-Rex");
12const feedRexMeat = feedRex("meat");
13
14console.log(feedRexMeat(50)); // T-Rex dostaje 50kg meat
15console.log(feedRexMeat(30)); // T-Rex dostaje 30kg meat
16
17const feedBrachio = feedDinosaur("Brachiosaurus");
18console.log(feedBrachio("plants")(200)); // Brachiosaurus dostaje 200kg plants
19
20// --- PRZYKLAD 2: Uniwersalna funkcja curry ---
21console.log("\n--- Uniwersalny curry ---");
22
23function curry(fn) {
24 return function curried(...args) {
25 if (args.length >= fn.length) {
26 return fn(...args);
27 }
28 return (...moreArgs) => curried(...args, ...moreArgs);
29 };
30}
31
32function createEnclosure(zone, type, capacity, voltage) {
33 return { zone, type, capacity, voltage };
34}
35
36const curriedCreate = curry(createEnclosure);
37
38// Rozne sposoby wywolania
39const r1 = curriedCreate("A")("carnivore")(5)(10000);
40const r2 = curriedCreate("A", "carnivore")(5, 10000);
41const r3 = curriedCreate("A", "carnivore", 5, 10000);
42
43console.log("Wywolanie 1:", JSON.stringify(r1));
44console.log("Wywolanie 2:", JSON.stringify(r2));
45console.log("Identyczne?", JSON.stringify(r1) === JSON.stringify(r2));
46
47// Specjalizowane fabryki
48const createCarnivoreZone = curriedCreate("X")("carnivore");
49console.log("Zone X-5:", JSON.stringify(createCarnivoreZone(5)(15000)));
50console.log("Zone X-10:", JSON.stringify(createCarnivoreZone(10)(20000)));
51
52// --- PRZYKLAD 3: Czesciowa aplikacja ---
53console.log("\n--- Czesciowa aplikacja ---");
54
55function partial(fn, ...presetArgs) {
56 return (...laterArgs) => fn(...presetArgs, ...laterArgs);
57}
58
59function logIncident(severity, zone, message) {
60 return `[${severity}] Zone ${zone}: ${message}`;
61}
62
63const logCritical = partial(logIncident, "CRITICAL");
64const logWarning = partial(logIncident, "WARNING");
65const logCriticalA = partial(logIncident, "CRITICAL", "A");
66
67console.log(logCritical("B", "Fence breach"));
68console.log(logWarning("A", "Sensor malfunction"));
69console.log(logCriticalA("T-Rex escaped!"));
70
71// --- PRZYKLAD 4: Currying z map/filter ---
72console.log("\n--- Currying w praktyce ---");
73
74const filterByField = curry((field, value, arr) =>
75 arr.filter((item) => item[field] === value)
76);
77
78const dinosaurs = [
79 { name: "Rex", diet: "carnivore", zone: "A" },
80 { name: "Blue", diet: "carnivore", zone: "A" },
81 { name: "Brachio", diet: "herbivore", zone: "B" },
82 { name: "Stego", diet: "herbivore", zone: "B" },
83];
84
85const filterByDiet = filterByField("diet");
86const getCarnivores = filterByDiet("carnivore");
87const getHerbivores = filterByDiet("herbivore");
88
89console.log("Drapiezniki:", getCarnivores(dinosaurs).map((d) => d.name));
90console.log("Roslinozerce:", getHerbivores(dinosaurs).map((d) => d.name));Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest currying w programowaniu funkcyjnym?
2. Czym różni się częściowa aplikacja (partial application) od curryingu?
Zadania praktyczne w grze
- Edytor kodu
Użyj uniwersalnej funkcji curry do stworzenia wyspecjalizowanych filtrów i loggerów.
- Układanie w pionie
Ułóż etapy tworzenia wyspecjalizowanej funkcji przez currying:
- Klikanie w kolejności
Ułóż elementy curried funkcji strzałkowej:
- Edytor kodu
Użyj partial() do stworzenia wyspecjalizowanych filtrów i połącz je w pipeline z pipe().