Kurs JavaScript i TypeScript · Moduł 12: Programowanie funkcyjne

Currying i częściowa aplikacja

5 min czytania
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 wynik

Każ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. 1. Czym jest currying w programowaniu funkcyjnym?

  2. 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().

Przydatne artykuły