În acest articol, vom vorbi despre închideri și funcții curri și ne vom juca cu aceste concepte pentru a construi abstracții minunate. Vreau să arăt ideea din spatele fiecărui concept, dar, de asemenea, să-l fac foarte practic cu exemple și cod refactorizat pentru a-l face mai distractiv.

Închideri

Închiderile sunt un subiect obișnuit în JavaScript și este cel cu care vom începe. Conform MDN:

O închidere este combinația unei funcții grupate împreună (închisă) cu referințe la starea sa înconjurătoare (mediul lexical).

Practic, de fiecare dată când se creează o funcție, se creează și o închidere și oferă acces la stare (variabile, constante, funcții și așa mai departe). Statul înconjurător este cunoscut sub numele de lexical environment.

Să arătăm un exemplu simplu:

function makeFunction() {
  const name="TK";
  function displayName() {
    console.log(name);
  }
  return displayName;
};

Ce avem noi aici?

  • Funcția noastră principală este numită makeFunction
  • O constantă numită name este atribuit cu șirul, 'TK'
  • Definiția displayName funcție (care doar înregistrează name constant)
  • Și, în sfârșit, makeFunction returnează displayName funcţie

Aceasta este doar o definiție a unei funcții. Când apelăm la makeFunction, va crea totul în el: o constantă și o altă funcție, în acest caz.

După cum știm, când displayName funcția este creată, este creată și închiderea și face funcția conștientă de mediul său, în acest caz, name constant. Acesta este motivul pentru care putem console.log name constantă fără a sparge nimic. Funcția știe despre mediul lexical.

const myFunction = makeFunction();
myFunction(); // TK

Grozav! Funcționează așa cum era de așteptat. Valoarea returnată a makeFunction este o funcție pe care o stocăm în myFunction constant. Când sunăm myFunction, se afișează TK.

De asemenea, îl putem face să funcționeze ca o funcție săgeată:

const makeFunction = () => {
  const name="TK";
  return () => console.log(name);
};

Dar dacă vrem să trecem numele și să-l afișăm? Simplu! Utilizați un parametru:

const makeFunction = (name="TK") => {
  return () => console.log(name);
};

// Or as a one-liner
const makeFunction = (name="TK") => () => console.log(name);

Acum ne putem juca cu numele:

const myFunction = makeFunction();
myFunction(); // TK

const myFunction = makeFunction('Dan');
myFunction(); // Dan

myFunction este conștient de argumentul transmis și dacă este o valoare implicită sau dinamică.

Închiderea se asigură că funcția creată nu este doar conștientă de constante / variabile, ci și de alte funcții din cadrul funcției.

Deci, acest lucru funcționează și:

const makeFunction = (name="TK") => {
  const display = () => console.log(name);
  return () => display();
};

const myFunction = makeFunction();
myFunction(); // TK

Funcția returnată știe despre display funcție și este capabil să o numească.

O tehnică puternică este utilizarea de închideri pentru a construi funcții și variabile „private”.

Acum câteva luni învățam structurile de date (din nou!) Și doream să le implementez pe fiecare. Dar foloseam întotdeauna abordarea orientată pe obiecte. Ca un pasionat de programare funcțională, am vrut să construiesc toate structurile de date urmând principiile FP (funcții pure, imuabilitate, transparență referențială etc.).

Prima structură de date pe care am învățat-o a fost Stack. Este destul de simplu. API-ul principal este:

  • push: adăugați un element pe primul loc al stivei
  • pop: eliminați primul element din stivă
  • peek: obține primul articol din stivă
  • isEmpty: verificați dacă stiva este goală
  • size: obțineți numărul de articole pe care le are stiva

Am putea crea în mod clar o funcție simplă fiecărei „metode” și îi putem transmite datele stivei. Apoi ar putea utiliza / transforma datele și le poate returna.

Dar putem crea și o stivă cu date private și expunem doar metodele API. Să o facem!

const buildStack = () => {
  let items = [];

  const push = (item) => items = [item, ...items];
  const pop = () => items = items.slice(1);
  const peek = () => items[0];
  const isEmpty = () => !items.length;
  const size = () => items.length;

  return {
    push,
    pop,
    peek,
    isEmpty,
    size,
  };
};

Pentru că am creat items stiva în interiorul nostru buildStack funcție, este „privat”. Poate fi accesat numai în cadrul funcției. Numai în acest caz push, pop, și astfel s-ar putea atinge datele. Exact asta căutăm.

Și cum îl folosim? Asa:

const stack = buildStack();

stack.isEmpty(); // true

stack.push(1); // [1]
stack.push(2); // [2, 1]
stack.push(3); // [3, 2, 1]
stack.push(4); // [4, 3, 2, 1]
stack.push(5); // [5, 4, 3, 2, 1]

stack.peek(); // 5
stack.size(); // 5
stack.isEmpty(); // false

stack.pop(); // [4, 3, 2, 1]
stack.pop(); // [3, 2, 1]
stack.pop(); // [2, 1]
stack.pop(); // [1]

stack.isEmpty(); // false
stack.peek(); // 1
stack.pop(); // []
stack.isEmpty(); // true
stack.size(); // 0

Deci, atunci când stiva este creată, toate funcțiile sunt conștiente de items date. Dar în afara funcției, nu putem accesa aceste date. Este personal. Modificăm datele folosind API-ul încorporat al stivei.

Curry

„Currying este procesul de a lua o funcție cu mai multe argumente și de a o transforma într-o succesiune de funcții, fiecare cu un singur argument.”
Interviu frontend

Deci, imaginați-vă că aveți o funcție cu mai multe argumente: f(a, b, c). Folosind curry, realizăm o funcție f(a) care returnează o funcție g(b) care returnează o funcție h(c).

Pe scurt: f(a, b, c) -> f(a) => g(b) => h(c)

Să construim un exemplu simplu care să adauge două numere. Dar mai întâi, fără a curge:

const add = (x, y) => x + y;
add(1, 2); // 3

Grozav! Super simplu! Aici avem o funcție cu două argumente. Pentru a-l transforma într-o funcție curri, avem nevoie de o funcție care primește x și returnează o funcție care primește y și returnează suma ambelor valori.

const add = (x) => {
  function addY(y) {
    return x + y;
  }

  return addY;
};

Putem refactoriza addY într-o funcție săgeată anonimă:

const add = (x) => {
  return (y) => {
    return x + y;
  }
};

Sau simplificați-l prin construirea unei funcții săgeată de linie:

const add = (x) => (y) => x + y;

Aceste trei funcții diferite au același comportament: construiți o secvență de funcții cu un singur argument.

Cum îl putem folosi?

add(10)(20); // 30

La început, poate părea puțin ciudat, dar există o logică în spatele ei. add(10) returnează o funcție. Și numim această funcție cu 20 valoare.

Aceasta este la fel ca:

const addTen = add(10);
addTen(20); // 30

Și acest lucru este interesant. Putem genera funcții specializate apelând prima funcție. Imaginați-vă că vrem un increment funcţie. Îl putem genera din add funcționează prin trecere 1 ca valoare.

const increment = add(1);
increment(9); // 10

Când puneam în aplicare Lazy Cypress, o bibliotecă npm pentru a înregistra comportamentul utilizatorului pe o pagină de formular și a genera cod de testare Cypress, am vrut să construiesc o funcție pentru a genera acest șir input[data-testid="123"]. Așa că am avut elementul (input), atributul (data-testid), iar valoarea (123). Interpolarea acestui șir în JavaScript ar arăta astfel: ${element}[${attribute}="${value}"].

Prima mea implementare a fost să primesc aceste trei valori ca parametri și să returnez șirul interpolat de mai sus:

const buildSelector = (element, attribute, value) =>
  `${element}[${attribute}="${value}"]`;

buildSelector('input', 'data-testid', 123); // input[data-testid="123"]

Și a fost minunat. Am realizat ceea ce căutam.

Dar, în același timp, am vrut să construiesc o funcție mai idiomatică. Ceva în care aș putea scrie „Get element X cu atributul Y și valoarea ZDeci, dacă împărțim această frază în trei pași:

  • obțineți un element X“: get(x)
  • cu atributul Y“: withAttribute(y)
  • și valoarea Z“: andValue(z)

Ne putem transforma buildSelector(x, y, z) în get(x)withAttribute(y)andValue(z) prin utilizarea conceptului de curry.

const get = (element) => {
  return {
    withAttribute: (attribute) => {
      return {
        andValue: (value) => `${element}[${attribute}="${value}"]`,
      }
    }
  };
};

Aici folosim o idee diferită: returnarea unui obiect cu funcție ca valoare-cheie. Atunci putem realiza această sintaxă: get(x).withAttribute(y).andValue(z).

Și pentru fiecare obiect returnat, avem următoarea funcție și argument.

Timp de refactorizare! Scoateți return declarații:

const get = (element) => ({
  withAttribute: (attribute) => ({
    andValue: (value) => `${element}[${attribute}="${value}"]`,
  }),
});

Cred că arată mai frumos. Iată cum îl folosim:

const selector = get('input')
  .withAttribute('data-testid')
  .andValue(123);

selector; // input[data-testid="123"]

andValue funcția știe despre element și attribute valorează pentru că este conștient de mediul lexical, cum ar fi cu închiderile despre care am vorbit înainte.

De asemenea, putem implementa funcții folosind „curry parțial” separând primul argument de restul, de exemplu.

După ce am dezvoltat mult timp web, sunt foarte familiarizat cu API Web ascultător de evenimente. Iată cum să îl utilizați:

const log = () => console.log('clicked');
button.addEventListener('click', log);

Am vrut să creez o abstracție pentru a construi ascultători de evenimente specializați și să îi folosesc prin trecerea elementului și a unui handler callback.

const buildEventListener = (event) => (element, handler) => element.addEventListener(event, handler);

În acest fel pot crea diferiți ascultători de evenimente specializați și îi pot folosi ca funcții.

const onClick = buildEventListener('click');
onClick(button, log);

const onHover = buildEventListener('hover');
onHover(link, log);

Cu toate aceste concepte, aș putea crea o interogare SQL folosind sintaxa JavaScript. Am vrut să întreb date JSON de acest gen:

const json = {
  "users": [
    {
      "id": 1,
      "name": "TK",
      "age": 25,
      "email": "tk@mail.com"
    },
    {
      "id": 2,
      "name": "Kaio",
      "age": 11,
      "email": "kaio@mail.com"
    },
    {
      "id": 3,
      "name": "Daniel",
      "age": 28,
      "email": "dani@mail.com"
    }
  ]
}

Așa că am construit un motor simplu pentru a gestiona această implementare:

const startEngine = (json) => (attributes) => ({ from: from(json, attributes) });

const buildAttributes = (node) => (acc, attribute) => ({ ...acc, [attribute]: node[attribute] });

const executeQuery = (attributes, attribute, value) => (resultList, node) =>
  node[attribute] === value
    ? [...resultList, attributes.reduce(buildAttributes(node), {})]
    : resultList;

const where = (json, attributes) => (attribute, value) =>
  json
    .reduce(executeQuery(attributes, attribute, value), []);

const from = (json, attributes) => (node) => ({ where: where(json[node], attributes) });

Cu această implementare, putem porni motorul cu datele JSON:

const select = startEngine(json);

Și utilizați-l ca o interogare SQL:

select(['id', 'name'])
  .from('users')
  .where('id', 1);

result; // [{ id: 1, name: 'TK' }]

Asta e pentru astăzi. Aș putea continua să vă arăt o mulțime de exemple diferite de abstracții, dar vă voi lăsa să vă jucați cu aceste concepte.

Puteți alte articole de genul acesta pe blogul meu.

Ale mele Stare de nervozitate și Github.

Resurse