Curry ja funktion koostumus

Tupakointikuutiot savuksi - MattysFlicks - (CC BY 2.0)
Huomaa: Tämä on osa ”Composing Software” -sarjaa (nyt kirja!), Joka käsittelee toiminnallisen ohjelmoinnin ja koostumustekniikan tekniikoiden käyttöä JavaScriptES6 +: ssa alusta alkaen. Pysy kanavalla. Tulevaa on paljon enemmän!
Osta kirja | Hakemisto |

Kun funktionaalinen ohjelmointi dramaattisesti nousee valtavirran JavaScript-ohjelmaan, kaarevista toiminnoista on tullut yleisiä monissa sovelluksissa. On tärkeää ymmärtää, millaiset he ovat, miten ne toimivat ja kuinka käyttää niitä hyväkseen.

Mikä on karvattu funktio?

Curryfunktio on funktio, joka ottaa useita argumentteja kerrallaan. Kun funktiona on 3 parametria, curryttu versio ottaa yhden argumentin ja palauttaa funktion, joka ottaa seuraavan argumentin, joka palauttaa funktion, joka ottaa kolmannen argumentin. Viimeinen funktio palauttaa funktion soveltamisen tuloksen kaikkiin argumentteihinsa.

Voit tehdä saman asian enemmän tai vähemmän parametreilla. Esimerkiksi, kun annetaan kaksi numeroa, a ja b karhennetussa muodossa, palauta a ja b summa:

// lisää = a => b => numero
const add = a => b => a + b;

Jotta sitä voidaan käyttää, meidän on käytettävä molempia toimintoja käyttämällä funktiosovelluksen syntaksia. JavaScript: suluissa () funktion viittauksen jälkeen laukaistaan ​​toiminnan kutsuminen. Kun funktio palauttaa toisen funktion, palautettuun funktioon voidaan välittömästi vedota lisäämällä ylimääräinen sulkujoukko:

const tulos = lisää (2) (3); // => 5

Ensin funktio vie a: n ja palauttaa sitten uuden funktion, joka sitten ottaa b: n palauttaa summan a ja b. Jokainen argumentti otetaan yksi kerrallaan. Jos toiminnolla olisi enemmän parametreja, se voisi yksinkertaisesti jatkaa uusien toimintojen palauttamista, kunnes kaikki argumentit toimitetaan ja sovellus voidaan suorittaa loppuun.

Lisäysfunktio ottaa yhden argumentin ja palauttaa sitten itsensä osittaisen sovelluksen kiinteänä sulkemisalueella. Sulkeminen on toiminto, joka on yhdistetty sen leksiseen laajuuteen. Sulkemiset luodaan suorituksen aikana toiminnan luomisen aikana. Kiinteä tarkoittaa, että muuttujille annetaan arvot sulkemisen yhdistetyssä laajuudessa.

Yllä olevan esimerkin suluissa esitetään funktiokutsut: add kutsutaan 2: lla, joka palauttaa osittain sovelletun funktion kiinteällä 2: lle. Sen sijaan, että määrittäisimme palautusarvon muuttujalle tai muuten käyttäisimme sitä, kutsumme palautettuun funktioon välittömästi 3 suluissa, joka täyttää sovelluksen ja palauttaa 5.

Mikä on osittainen hakemus?

Osittainen sovellus on funktio, jota on sovellettu joihinkin, mutta ei kaikkiin sen perusteisiin. Toisin sanoen, se on toiminto, jolla on joitain perusteita kiinteästi sulkemisalueensa sisäpuolelle. Toiminnon, jonka jotkut sen parametreista ovat kiinteitä, sanotaan sovellettavan osittain.

Mitä eroa?

Osittaiset sovellukset voivat viedä niin monta tai niin vähän argumenttia kerrallaan kuin halutaan. Toisaalta currytut toiminnot palauttavat aina yhdenarvon funktion: funktio, joka ottaa yhden argumentin.

Kaikki kaarevat toiminnot palauttavat osittaiset sovellukset, mutta kaikki osittaiset sovellukset eivät ole seurausta kaarevista toiminnoista.

Yhtenäinen vaatimus currytuista toiminnoista on tärkeä ominaisuus.

Mikä on ilmainen tyyli?

Pisteettömä tyyli on ohjelmointityyli, jossa funktiomääritelmissä ei viitata funktion argumentteihin. Katsotaanpa funktion määritelmiä JavaScript:

funktio foo (/ * parametrit ilmoitetaan täällä * /) {
  // ...
}
const foo = (/ * parametrit ilmoitetaan täällä * /) => // ...
const foo = funktio (/ * parametrit ilmoitetaan täällä * /) {
  // ...
}

Kuinka voit määritellä toiminnot JavaScript-ohjelmassa viittamatta vaadittuihin parametreihin? Emme voi käyttää toimintoavainsanaa, emmekä voi käyttää nuolifunktiota (=>), koska ne edellyttävät minkä tahansa muodollisen parametrin ilmoittamista (mikä viittaa sen argumentteihin). Joten meidän täytyy sen sijaan kutsua toiminto, joka palauttaa funktion.

Luo toiminto, joka kasvattaa numeroa, jonka sille siirrät yhdellä, pistemättömällä tyylillä. Muista, että meillä on jo funktio nimeltään add, joka ottaa luvun ja palauttaa osittain sovelletun toiminnon, jonka ensimmäinen parametri on kiinnitetty mihin tahansa syötät. Voimme käyttää sitä luomaan uuden funktion nimeltä inc ():

// inc = n => numero
// Lisää 1 mihin tahansa numeroon.
const inc = add (1);
inc (3); // => 4

Tästä tulee mielenkiintoinen yleistämis- ja erikoistumismekanismina. Palautettu toiminto on vain erikoistunut versio yleisimmästä add () -toiminnosta. Lisää () avulla voidaan luoda niin monta erikoisversiota kuin haluamme:

const inc10 = lisää (10);
const inc20 = lisää (20);
inc10 (3); // => 13
inc20 (3); // => 23

Ja tietenkin, näillä kaikilla on omat sulkemisalueensa (sulkemiset luodaan funktion luomishetkellä - kun lisäys () käynnistetään), joten alkuperäinen inc () toimii edelleen:

sis. (3) // 4

Kun luot inc () funktiokutsun add (1) kanssa, add (): n sisällä oleva parametri kiinnittyy arvoon 1 palautetun funktion sisällä, joka määritetään inc: lle.

Sitten kun kutsumme inc (3), add (): n sisällä oleva b-parametri korvataan argumenttiarvolla 3, ja sovellus valmistuu, palauttaen summan 1 ja 3.

Kaikki kaarevat toiminnot ovat eräänlainen korkeamman asteen toiminto, jonka avulla voit luoda erikoistuneita versioita alkuperäisestä toiminnosta kyseessä olevaan käyttötapaukseen.

Miksi me currya?

Currytut toiminnot ovat erityisen hyödyllisiä funktion koostumuksen yhteydessä.

Algebran ollessa annettu kaksi funktiota, g ja f:

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

Voit yhdistää nämä toiminnot yhdessä luodaksesi uuden funktion, h suorasta c: ään:

// Algebra-määritelmä, lainaamalla "." -Koostumuksen operaattori
// Haskelliltä
h: a -> c
h = f. g = f (g (x))

JavaScript:

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

Algebran määritelmä:

f. g = f (g (x))

Voidaan kääntää JavaScript:

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

Mutta se pystyisi vain sävelttämään kaksi toimintoa kerrallaan. Algebrassa on mahdollista kirjoittaa:

f. g. h

Voimme kirjoittaa toiminnon laatia niin monta funktiota kuin haluat. Toisin sanoen, kirjoita () luo putkilinjan toimintoja, joiden yhden toiminnon lähtö on kytketty seuraavan tuloon.

Kirjoitan sen yleensä seuraavasti:

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

Tämä versio ottaa määrän funktioita ja palauttaa funktion, joka ottaa alkuperäisen arvon, ja käyttää sitten reduRight () iteroidaksesi oikealta vasemmalle kunkin funktion, f, fns: ssä, ja soveltaa sitä vuorotellen kertyneeseen arvoon y . Mitä keräämme akun kanssa, y tässä funktiossa on komennon () palauttaman funktion palautusarvo.

Nyt voimme kirjoittaa sävellyksemme näin:

const g = n => n + 1;
const f = n => n * 2;
// korvata `x => f (g (x))` sanoilla `säveltää (f, g)`
const h = säveltää (f, g);
h (20); // => 42

Jäljittää

Pisteettömää tyyliä käyttävä toimintojen koostumus luo erittäin tiiviin, luettavan koodin, mutta se voi tulla helposti virheenkorjauksen kustannuksella. Entä jos haluat tarkistaa toimintojen väliset arvot? trace () on kätevä apuohjelma, jonka avulla voit tehdä juuri sen. Se on muodoltaan kaareva funktio:

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

Nyt voimme tarkastaa putkilinjan:

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Huomaa: funktioiden sovellusjärjestys on
pohja-to-top:
* /
const h = säveltää (
  jäljittää ('f: n jälkeen'),
  f,
  jälki ('g: n jälkeen'),
  g
);
h (20);
/ *
g: n jälkeen: 21
f: 42 jälkeen
* /

compose () on hieno apuohjelma, mutta kun meidän on sävellettävä enemmän kuin kaksi toimintoa, on joskus kätevää, jos pystymme lukemaan ne ylhäältä alas. Voimme tehdä sen kääntämällä järjestyksen, johon funktiot kutsutaan. Siellä on toinen koostumusapuohjelma nimeltään pipe (), joka säveltää käänteisessä järjestyksessä:

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

Nyt voimme kirjoittaa yllä olevan koodin näin:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Nyt funktion sovellusjärjestys
kulkee ylhäältä alas:
* /
const h = putki (
  g,
  jälki ('g: n jälkeen'),
  f,
  jäljittää ('f: n jälkeen'),
);
h (20);
/ *
g: n jälkeen: 21
f: 42 jälkeen
* /

Curry ja tehtävän koostumus yhdessä

Jopa funktion koostumuksen ulkopuolella, curry on varmasti hyödyllinen abstraktio, jota voimme käyttää erikoistamaan toimintoja. Esimerkiksi kartan () karhennettu versio voi olla erikoistunut suorittamaan monia erilaisia ​​asioita:

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 onEven = n => n% 2 === 0;
const stripe = n => onko joku (n)? 'tumma valo';
const stripeAll = kartta (raita);
const raidallinen = stripeAll (arr);
log (raidallinen);
// => ["vaalea", "tumma", "vaalea", "tumma"]
const double = n => n * 2;
const doubleAll = kartta (kaksinkertainen);
const kaksinkertaistui = doubleAll (arr);
log (kaksinkertainen);
// => [2, 4, 6, 8]

Karvaisten funktioiden todellinen voima on kuitenkin se, että ne yksinkertaistavat funktion koostumusta. Toiminto voi ottaa minkä tahansa määrän tuloja, mutta voi palauttaa vain yhden ulostulon. Jotta toiminnot olisivat kompostoitavia, lähtötyypin on oltava linjassa odotetun tulotyypin kanssa:

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

Jos g-funktio odotettavissa olevan kahden parametrin yläpuolella, f: n lähtö ei vastaa g: n tuloa:

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

Kuinka saamme x: n g: ksi tässä skenaariossa? Yleensä vastaus on curry g.

Muista, että käyristyneen funktion määritelmä on funktio, joka ottaa useita parametreja kerrallaan ottamalla ensimmäisen argumentin ja palauttamalla sarjan toimintoja, jotka kullakin seuraavalla argumentilla, kunnes kaikki parametrit on kerätty.

Määritelmän avainsanat ovat ”yksi kerrallaan”. Syy siihen, että currytut toiminnot ovat niin käteviä funktion koostumukselle, on se, että ne muuntavat funktiot, jotka odottavat useita parametreja, funktioiksi, jotka voivat ottaa yhden argumentin, jolloin ne mahtuvat funktion koostumuksen putkistoon. Ota esimerkki jäljitys () -toiminnosta aikaisemmasta:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
const h = putki (
  g,
  jälki ('g: n jälkeen'),
  f,
  jäljittää ('f: n jälkeen'),
);
h (20);
/ *
g: n jälkeen: 21
f: 42 jälkeen
* /

trace () määrittelee kaksi parametria, mutta ottaa ne yksi kerrallaan, jotta voimme erikoistua funktioon inline. Jos jälkiä () ei ole curryttu, emme voineet käyttää sitä tällä tavalla. Meidän on kirjoitettava putki seuraavasti:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = (tarra, arvo) => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
const h = putki (
  g,
  // jäljitys () -puhelut eivät ole enää pistettömiä,
  // johdetaan välimuuttuja `x`.
  x => jäljitys ('jälkeen g', x),
  f,
  x => jäljitys ('f': n jälkeen, x),
);
h (20);

Mutta pelkästään funktion karruttaminen ei riitä. Sinun on myös varmistettava, että toiminto odottaa parametrejä oikeassa järjestyksessä niiden erikoistamiseksi. Katso mitä tapahtuu, jos curry jäljitä () uudelleen, mutta käännä parametrijärjestys:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = value => label => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
const h = putki (
  g,
  // jäljitys () -puhelut eivät voi olla pisteettömiä,
  // koska argumentteja odotetaan väärässä järjestyksessä.
  x => jälki (x) ('jälkeen g'),
  f,
  x => jäljitys (x) ('f: n jälkeen),
);
h (20);

Jos olet nipissä, voit korjata ongelman flip () -nimellä, joka yksinkertaisesti vaihtaa kahden parametrin järjestystä:

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

Nyt voimme kehittää käännetyn Trace () -toiminnon:

const flippedTrace = läppä (jäljitys);

Ja käytä sitä näin:

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}`);
  palautusarvo;
};
const flippedTrace = läppä (jäljitys);
const g = n => n + 1;
const f = n => n * 2;
const h = putki (
  g,
  flippedTrace ('jälkeen g'),
  f,
  flippedTrace ('f: n jälkeen'),
);
h (20);

Mutta parempi tapa on kirjoittaa toiminto oikein ensin. Tyyliä kutsutaan joskus ”data last”, mikä tarkoittaa, että sinun on otettava ensin erikoistuneet parametrit ja otettava tiedot, jotka toiminto viimeksi toimii. Se antaa meille funktion alkuperäisen muodon:

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

Jokainen jäljityksen () käyttö etiketissä luo erikoistuneen version jäljitysfunktiosta, jota käytetään putkilinjassa, jossa tarra kiinnitetään palautetun osittaisen jäljen sovelluksen sisään. Siis tämä:

const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const traceAfterG = jäljitys ('g jälkeen');

... vastaa tätä:

const traceAfterG = arvo => {
  const-tarra = 'g: n jälkeen';
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};

Jos vaihdimme jäljen ('g jälkeen') traceAfterG: lle, se tarkoittaisi samaa:

const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
// Jäljitellyn kaareva versio ()
// säästää meitä kirjoittamasta kaikkea tätä koodia ...
const traceAfterG = arvo => {
  const-tarra = 'g: n jälkeen';
  console.log (`$ {label}: $ {value}`);
  palautusarvo;
};
const g = n => n + 1;
const f = n => n * 2;
const h = putki (
  g,
  traceAfterG,
  f,
  jäljittää ('f: n jälkeen'),
);
h (20);

johtopäätös

Curryfunktio on funktio, joka ottaa useita parametrejä kerrallaan ottamalla ensimmäisen argumentin ja palauttamalla sarjan toimintoja, jotka molemmat ottavat seuraavan argumentin, kunnes kaikki parametrit on korjattu ja funktiosovellus voi suorittaa loppuun, jolloin piste, tuloksena oleva arvo palautetaan.

Osittainen hakemus on toiminto, jota on jo sovellettu joihinkin - mutta ei vielä kaikkiin - argumentteihin. Argumentteja, joihin funktiota on jo sovellettu, kutsutaan kiinteiksi parametreiksi.

Pisteettömä tyyli on tapa määritellä funktio viittamatta sen argumentteihin. Yleensä pisteettömä toiminto luodaan kutsumalla funktio, joka palauttaa funktion, kuten esimerkiksi curred-funktion.

Currytut toiminnot ovat loistavia funktion koostumukselle, koska niiden avulla voit helposti muuntaa n-ary-funktion funktion koostumuksen putkistojen tarvittavaan yksiarvoiseen funktiomuotoon: Putkilinjan funktioiden on odotettava tarkalleen yhtä argumenttia.

Tietojen viimeiset toiminnot ovat käteviä toimintojen koostumukselle, koska niitä voidaan käyttää helposti pisteettömässä muodossa.

Osta kirja Hakemisto |

Lisätietoja EricElliottJS.com

EricElliottJS.com-sivuston jäsenille on tarjolla videotunteja interaktiivisilla koodihaasteilla. Jos et ole jäsen, kirjaudu tänään.

Eric Elliott on hajautettu järjestelmäasiantuntija ja kirjojen kirjoittaminen “Composing Software” ja “Programming JavaScript Applications”. DevAnywhere.io: n perustajana hän opettaa kehittäjille taitoja, joita he tarvitsevat etätyöskentelyyn ja työ- ja yksityiselämän tasapainon omaksumiseen. Hän rakentaa ja neuvoo kehitystyöryhmiä salausprojekteihin ja on osallistunut ohjelmistokokemuksiin Adobe Systemsille, Zumba Fitnessille, The Wall Street Journalille, ESPN: lle, BBC: lle ja huipputalostajille, mukaan lukien Usher, Frank Ocean, Metallica ja monille muille.

Hän nauttii kauko-elämäntavasta maailman kauneimman naisen kanssa.