Curry og funktionskomposition

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Bemærk: Dette er en del af serien "Komponerende software" (nu en bog!) Om indlæring af funktionel programmering og kompositionssoftwareteknikker i JavaScriptES6 + fra bunden af. Bliv hængende. Der kommer meget mere af dette!
Køb bogen | Indeks |

Med den dramatiske stigning i funktionel programmering i mainstream JavaScript er curried-funktioner blevet almindelige i mange applikationer. Det er vigtigt at forstå, hvad de er, hvordan de fungerer, og hvordan de bruges godt.

Hvad er en curried funktion?

En curried-funktion er en funktion, der tager flere argumenter ad gangen. Givet en funktion med 3 parametre, tager den curry version et argument og returnerer en funktion, der tager det næste argument, som returnerer en funktion, der tager det tredje argument. Den sidste funktion returnerer resultatet af anvendelsen af ​​funktionen til alle dens argumenter.

Du kan gøre det samme med flere eller færre parametre. For eksempel, givet to tal, a og b i curry form, returnerer summen af ​​a og b:

// tilføj = a => b => Antal
const add = a => b => a + b;

For at bruge det, skal vi anvende begge funktioner ved hjælp af syntaxen til funktionsapplikationen. I JavaScript udløser parenteserne () efter funktionsreferencen aktivering af funktion. Når en funktion returnerer en anden funktion, kan den returnerede funktion straks aktiveres ved at tilføje et ekstra sæt parenteser:

const resultat = tilføj (2) (3); // => 5

Først tager funktionen a, og returnerer derefter en ny funktion, der derefter tager b returnerer summen af ​​a og b. Hvert argument tages én ad gangen. Hvis funktionen havde flere parametre, kunne den simpelthen fortsætte med at returnere nye funktioner, indtil alle argumenterne er leveret, og applikationen kan afsluttes.

Tilføjfunktionen tager ét argument og returnerer derefter en delvis anvendelse af sig selv med en fast i lukningsomfanget. En lukning er en funktion bundet med dens leksikale rækkevidde. Lukninger oprettes ved kørsel under oprettelse af funktioner. Fast betyder, at variablerne er tildelt værdier i lukningens bundlede omfang.

Parenteserne i eksemplet ovenfor repræsenterer funktionsindkaldelser: tilføjelse påkaldes med 2, der returnerer en delvist anvendt funktion med en fast til 2. I stedet for at tildele returværdien til en variabel eller på anden måde bruge den, påkalder vi straks den returnerede funktion ved at videregive 3 til det i parentes, der afslutter applikationen og returnerer 5.

Hvad er en delvis anvendelse?

En delvis anvendelse er en funktion, der er anvendt på nogle, men endnu ikke alle dens argumenter. Med andre ord, det er en funktion, der har nogle argumenter rettet inden for dens lukningsomfang. Det siges, at en funktion med nogle af dens parametre er delvist anvendt.

Hvad er forskellen?

Delvise applikationer kan tage så mange eller så få argumenter en gang som ønsket. Curried-funktioner på den anden side returnerer altid en unary funktion: en funktion, der tager ét argument.

Alle curried-funktioner returnerer delvise applikationer, men ikke alle delvise applikationer er resultatet af curried-funktioner.

Det unære krav til curryfunktioner er en vigtig funktion.

Hvad er punktfri stil?

Punktfri stil er en programmeringsstil, hvor funktionsdefinitioner ikke henviser til funktionens argumenter. Lad os se på funktionsdefinitioner i JavaScript:

funktion foo (/ * parametre er erklæret her * /) {
  // ...
}
const foo = (/ * parametre er deklareret her * /) => // ...
const foo = funktion (/ * parametre er deklareret her * /) {
  // ...
}

Hvordan kan du definere funktioner i JavaScript uden at henvise til de krævede parametre? Vi kan ikke bruge funktionstastaturet, og vi kan ikke bruge en pilefunktion (=>), fordi disse kræver formelle parametre, der skal deklareres (som henviser til dens argumenter). Så hvad vi skal gøre i stedet er at kalde en funktion, der returnerer en funktion.

Opret en funktion, der øger det antal, du sender til det, ved hjælp af en punktfri stil. Husk, vi har allerede en funktion kaldet add, der tager et tal og returnerer en delvist anvendt funktion med sin første parameter fastgjort til hvad du sender i. Vi kan bruge den til at oprette en ny funktion kaldet inc ():

// inc = n => Antal
// Tilføjer 1 til ethvert nummer.
const inc = tilføj (1);
inc (3); // => 4

Dette bliver interessant som en mekanisme til generalisering og specialisering. Den returnerede funktion er bare en specialiseret version af den mere generelle add () -funktion. Vi kan bruge add () til at oprette så mange specialiserede versioner, som vi ønsker:

const inc10 = tilføj (10);
const inc20 = tilføj (20);
inc10 (3); // => 13
inc20 (3); // => 23

Og selvfølgelig har disse alle deres egne lukningsomfang (lukninger oprettes på tidspunktet for oprettelse af funktion - når tilføjelse () tilkaldes), så den oprindelige inc () fortsætter med at fungere:

inc (3) // 4

Når vi opretter inc () med funktionen call add (1), bliver en parameter inde i add () fastgjort til 1 inde i den returnerede funktion, der får tildelt inc.

Når vi derefter kalder inc (3), erstattes b-parameteren inde i add () med argumentværdien, 3, og applikationen afsluttes og returnerer summen af ​​1 og 3.

Alle karryfunktioner er en form for højere ordensfunktion, som giver dig mulighed for at oprette specialiserede versioner af den originale funktion til den specifikke brugssag.

Hvorfor karrer vi?

Karryfunktioner er især nyttige i sammenhæng med funktionskomposition.

I algebra, givet to funktioner, g og f:

g: a -> b
f: b -> c

Du kan komponere disse funktioner sammen for at oprette en ny funktion, h fra en direkte til c:

// Algebra-definition, hvor du låner kompositionoperatøren
// fra Haskell
h: a -> c
h = f. g = f (g (x))

I JavaScript:

const g = n => n + 1;
const f = n => n * 2;
const h = x => f (g (x));
h (20); // => 42

Definition af algebra:

f. g = f (g (x))

Kan oversættes til JavaScript:

const compose = (f, g) => x => f (g (x));

Men det ville kun være i stand til at komponere to funktioner ad gangen. I algebra er det muligt at skrive:

f. g. h

Vi kan skrive en funktion til at komponere så mange funktioner, som du vil. Med andre ord opretter compose () en pipeline af funktioner med output fra en funktion tilsluttet input til den næste.

Det er sådan, jeg normalt skriver det:

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);

Denne version tager et vilkårligt antal funktioner og returnerer en funktion, der tager startværdien, og bruger derefter reduceright () til at iterere fra højre til venstre over hver funktion, f, i fns, og anvende den efter tur til den akkumulerede værdi, y . Det, vi akkumulerer med akkumulatoren, y i denne funktion er returværdien for den funktion, der returneres ved at skrive ().

Nu kan vi skrive vores komposition sådan:

const g = n => n + 1;
const f = n => n * 2;
// erstatt `x => f (g (x))` med `komponere (f, g)`
const h = komponere (f, g);
h (20); // => 42

Trace

Funktionssammensætning ved hjælp af punktfri stil skaber meget kortfattet, læsbar kode, men den kan komme til bekostning af let fejlsøgning. Hvad hvis du vil inspicere værdierne mellem funktioner? trace () er et praktisk værktøj, der giver dig mulighed for at gøre netop det. Det har form af en curry funktion:

const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};

Nu kan vi inspicere rørledningen:

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Bemærk: Funktionsapplikationsordren er
bottom-til-top:
* /
const h = komponere (
  spor ('efter f'),
  f,
  spor ('efter g'),
  g
);
h (20);
/ *
efter g: 21
efter f: 42
* /

compose () er et fantastisk værktøj, men når vi har brug for at komponere mere end to funktioner, er det undertiden praktisk, hvis vi kan læse dem i top-til-bund-rækkefølge. Vi kan gøre det ved at vende den rækkefølge, som funktionerne kaldes. Der er et andet kompositionværktøj kaldet pipe (), der komponerer i omvendt rækkefølge:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);

Nu kan vi skrive ovenstående kode sådan:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Nu er funktion applikations rækkefølge
kører top-til-bund:
* /
const h = rør (
  g,
  spor ('efter g'),
  f,
  spor ('efter f'),
);
h (20);
/ *
efter g: 21
efter f: 42
* /

Curry og funktionskomposition, sammen

Selv uden for kontekst af funktionskomposition er currying bestemt en nyttig abstraktion, vi kan bruge til at specialisere funktioner. For eksempel kan en curried version af kort () være specialiseret til at gøre mange forskellige ting:

const map = fn => mappable => mappable.map (fn);
const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const log = (... args) => console.log (... args);
const arr = [1, 2, 3, 4];
const isEven = n => n% 2 === 0;
const stripe = n => isEven (n)? 'mørkt lys';
const stripeAlle = kort (stripe);
const striped = stripeAll (arr);
log (stribet);
// => ["lys", "mørk", "lys", "mørk"]
const dobbelt = n => n * 2;
const doubleAll = kort (dobbelt);
const fordoblet = doubleAll (arr);
log (fordoblet);
// => [2, 4, 6, 8]

Men den virkelige magt ved curryfunktioner er, at de forenkler funktionskompositionen. En funktion kan tage et hvilket som helst antal input, men kan kun returnere en enkelt output. For at funktioner kan være komposible, skal outputtypen justeres med den forventede inputtype:

f: a => b
g: b => c
h: a => c

Hvis g-funktionen ovenfor forventede to parametre, ville output fra f ikke være på linje med input for g:

f: a => b
g: (x, b) => c
h: a => c

Hvordan får vi x til g i dette scenarie? Normalt er svaret at curry g.

Husk, at definitionen af ​​en curried-funktion er en funktion, der tager flere parametre ad gangen ved at tage det første argument og returnere en række funktioner, som hver tager det næste argument, indtil alle parametrene er samlet.

Nøgleordene i denne definition er "én ad gangen". Årsagen til, at curried-funktioner er så praktiske for funktionskomposition er, at de transformerer funktioner, der forventer flere parametre til funktioner, der kan tage et enkelt argument, hvilket giver dem mulighed for at passe ind i en pipeline for funktionskomposition. Tag funktionen trace () som et eksempel fra tidligere:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
const h = rør (
  g,
  spor ('efter g'),
  f,
  spor ('efter f'),
);
h (20);
/ *
efter g: 21
efter f: 42
* /

trace () definerer to parametre, men tager dem en ad gangen, så vi kan specialisere funktionen inline. Hvis spor () ikke blev curry, kunne vi ikke bruge det på denne måde. Vi bliver nødt til at skrive pipeline sådan:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = (etiket, værdi) => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
const h = rør (
  g,
  // spor () -opkaldene er ikke længere pointfri,
  // introduktion af mellemvariablen, `x`.
  x => spor ('efter g', x),
  f,
  x => spor ('efter f', x),
);
h (20);

Men blot at karry en funktion er ikke nok. Du skal også sikre dig, at funktionen forventer parametre i den rigtige rækkefølge for at specialisere dem. Se hvad der sker, hvis vi curry trace () igen, men vend parameterordren:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = value => label => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
const h = rør (
  g,
  // spor () -opkaldene kan ikke være pointfrie,
  // fordi argumenter forventes i forkert rækkefølge.
  x => spor (x) ('efter g'),
  f,
  x => spor (x) ('efter f'),
);
h (20);

Hvis du er i en klemme, kan du løse dette problem med en funktion kaldet flip (), der blot vipper rækkefølgen af ​​to parametre:

const flip = fn => a => b => fn (b) (a);

Nu kan vi kasse en flippedTrace () -funktion:

const flippedTrace = flip (spor);

Og brug det sådan:

const flip = fn => a => b => fn (b) (a);
const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = value => label => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const flippedTrace = flip (spor);
const g = n => n + 1;
const f = n => n * 2;
const h = rør (
  g,
  flippedTrace ('efter g'),
  f,
  flippedTrace ('efter f'),
);
h (20);

Men en bedre tilgang er at skrive funktionen korrekt i første omgang. Stilen kaldes undertiden "data sidst", hvilket betyder, at du først skal tage specialiseringsparametrene og tage de data, som funktionen vil handle på sidst. Det giver os den originale form for funktionen:

const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};

Hver applikation af spor () på en etiket opretter en specialiseret version af den sporingsfunktion, der bruges i rørledningen, hvor etiketten er fastgjort inden for den returnerede delvise anvendelse af spor. Så dette:

const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const traceAfterG = trace ('efter g');

... svarer til dette:

const traceAfterG = value => {
  const label = 'efter g';
  console.log (`$ {label}: $ {value}`);
  returværdi;
};

Hvis vi byttet spor ('efter g') til traceAfterG, ville det betyde den samme ting:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
// Den curry version af spor ()
// redder os fra at skrive al denne kode ...
const traceAfterG = value => {
  const label = 'efter g';
  console.log (`$ {label}: $ {value}`);
  returværdi;
};
const g = n => n + 1;
const f = n => n * 2;
const h = rør (
  g,
  traceAfterG,
  f,
  spor ('efter f'),
);
h (20);

Konklusion

En curried-funktion er en funktion, der tager flere parametre ad gangen ved at tage det første argument og returnere en række funktioner, som hver tager det næste argument, indtil alle parametre er rettet, og funktionsapplikationen kan afsluttes, hvor punkt, returneres den resulterende værdi.

En delvis anvendelse er en funktion, der allerede er anvendt på nogle - men endnu ikke alle - af dens argumenter. De argumenter, som funktionen allerede er anvendt til, kaldes faste parametre.

Punktfri stil er en måde at definere en funktion uden henvisning til dens argumenter. Generelt oprettes en punktfri funktion ved at kalde en funktion, der returnerer en funktion, såsom en curried funktion.

Curried-funktioner er fremragende til funktionskomposition, fordi de giver dig mulighed for let at konvertere en n-ary-funktion til den unære funktionsform, der er nødvendig til rørledninger til funktionssammensætning: Funktioner i en rørledning skal forvente nøjagtigt et argument.

Data sidste funktioner er praktiske til funktionskomposition, fordi de let kan bruges i punktfri stil.

Køb bogen | Indeks |

Lær mere på EricElliottJS.com

Videotimer med interaktive kodeudfordringer er tilgængelige for medlemmer af EricElliottJS.com. Hvis du ikke er medlem, tilmeld dig i dag.

Eric Elliott er en distribueret systemekspert og forfatter af bøgerne, "Komponerende software" og "Programmering af JavaScript-applikationer". Som medstifter af DevAnywhere.io lærer han udviklere de færdigheder, de har brug for til at arbejde eksternt og omfatte balance mellem arbejde og liv. Han bygger og rådgiver udviklingshold til kryptoprojekter og har bidraget til softwareoplevelser for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC og topindspilningskunstnere, herunder Usher, Frank Ocean, Metallica og mange flere.

Han nyder en fjern livsstil med den smukkeste kvinde i verden.