Apr 23, 2018 – 10 min citește

ES6 a introdus un nou mod de a lucra cu funcții și iteratori sub forma generatoarelor (sau funcții generatoare). Un generator este o funcție care se poate opri la jumătatea drumului și apoi poate continua de unde s-a oprit. Pe scurt, un generator pare a fi o funcție, dar se comportă ca un iterator.

Fun Fact: async/await se poate baza pe generatoare. Citiți mai multe aici.

Generatorii sunt legați în mod complicat de iteratori. Dacă nu știți despre iteratori, iată un articol care să vă ajute să îi înțelegeți mai bine.

Iată o analogie simplă pentru a avea o intuiție pentru generatori înainte de a trece la detalii tehnice.

Imaginați-vă că citiți un techno-thriller înțepător. Cu totul absorbit de paginile cărții, abia dacă auziți soneria de la ușă. Este băiatul care livrează pizza. Vă ridicați pentru a deschide ușa. Cu toate acestea, înainte de a face acest lucru, vă puneți un semn de carte la ultima pagină citită. Îți salvezi mental evenimentele din intrigă. Apoi, te duci să îți iei pizza. După ce te întorci în camera ta, începi cartea de la pagina la care ai pus semnul de carte. Nu o începi din nou de la prima pagină. Într-un anumit sens, ați acționat ca o funcție generatoare.

Să vedem cum putem utiliza generatoarele pentru a rezolva unele probleme comune în timpul programării. Dar, înainte de asta, să definim ce sunt generatoarele.

Ce sunt generatoarele?

O funcție normală, cum ar fi aceasta, nu poate fi oprită înainte de a-și termina sarcina, adică de a fi executată ultima sa linie. Ea urmează ceva ce se numește modelul run-to-completion.

function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}

Singurul mod de a ieși din normalFunc este prin returnieșirea din el, sau throwieșirea unei erori. Dacă apelați din nou funcția, aceasta va începe din nou execuția de la început.

În schimb, un generator este o funcție care se poate opri la jumătatea drumului și apoi să continue de unde s-a oprit.

Iată alte câteva definiții comune ale generatoarelor –

  • Generatoarele sunt o clasă specială de funcții care simplifică sarcina de a scrie iteratori.
  • Un generator este o funcție care produce o secvență de rezultate în loc de o singură valoare, adică generează o serie de valori.

În JavaScript, un generator este o funcție care returnează un obiect asupra căruia puteți apela next(). Fiecare invocare a lui next() va returna un obiect de forma –

{ 
value: Any,
done: true|false
}

Proprietatea value va conține valoarea. Proprietatea done este fie true, fie false. Când done devine true, generatorul se oprește și nu va mai genera alte valori.

Iată o ilustrare a aceluiași lucru –

Funcții normale vs. generatoare

Rețineți săgeata punctată care închide bucla randament-rezumat-randament chiar înainte de Finish în partea Generators din imagine. Există posibilitatea ca un generator să nu se termine niciodată. Vom vedea un exemplu mai târziu.

Crearea unui generator

Să vedem cum putem crea un generator în JavaScript –

function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined

Concentrează-te pe părțile îngroșate. Pentru a crea o funcție de generator, folosim sintaxa function * în loc de doar function. Între cuvântul cheie function, * și numele funcției poate exista orice număr de spații. Deoarece este doar o funcție, o puteți folosi oriunde poate fi folosită o funcție, adică în interiorul obiectelor și în metodele claselor.

În interiorul corpului funcției, nu avem un return. În schimb, avem un alt cuvânt cheie yield (linia 2). Este un operator cu ajutorul căruia un generator se poate pune în pauză. De fiecare dată când un generator întâlnește un yield, acesta „returnează” valoarea specificată după el. În acest caz, se returnează Hello,. Cu toate acestea, noi nu spunem „returnat” în contextul generatoarelor. Spunem că „generatorul a dat Hello, „.”

De asemenea, ne putem întoarce de la un generator. Cu toate acestea, return setează proprietatea done la true, după care generatorul nu mai poate genera alte valori.

function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}

În linia 3, creăm obiectul generator. Se pare că invocăm funcția generatorFunction. Într-adevăr, așa facem! Diferența este că, în loc să returneze orice valoare, o funcție de generator returnează întotdeauna un obiect generator. Obiectul generator este un iterator. Deci îl puteți utiliza în buclele for-of sau în alte funcții care acceptă un iterabil.

În linia 4, apelăm metoda next() pe generatorObject. Cu acest apel, generatorul începe să se execute. În primul rând, acesta console.log pe This will be executed first. Apoi, întâlnește un yield 'Hello, '. Generatorul cedează valoarea sub forma unui obiect { value: 'Hello, ', done: false } și se suspendă/se oprește. Acum, acesta așteaptă următoarea invocare.

În linia 5, apelăm din nou next(). De data aceasta, generatorul se trezește și începe execuția de unde a rămas. Următoarea linie pe care o găsește este un console.log. Acesta înregistrează șirul I will be printed after the pause. Se întâlnește un alt yield. Valoarea este redată sub forma obiectului { value: 'World!', done: false }. Extragem proprietatea value și o înregistrăm. Generatorul doarme din nou.

În linia 6, invocăm din nou next(). De data aceasta nu mai sunt linii de executat. Amintiți-vă că fiecare funcție returnează implicit undefined dacă nu este furnizată o instrucțiune return. Prin urmare, generatorul returnează (în loc să cedeze) un obiect { value: undefined, done: true}. done este setat la true. Acest lucru semnalează sfârșitul acestui generator. Acum, acesta nu mai poate genera alte valori sau relua din nou, deoarece nu mai există alte instrucțiuni care să fie executate.

Va trebui să creăm un nou obiect generator pentru a rula din nou generatorul.

Utilizări ale generatoarelor

Există multe cazuri impresionante de utilizare a generatoarelor. Să vedem câteva dintre ele.

Implementarea Iterabilelor

Când implementați un iterator, trebuie să faceți manual un obiect iterator cu o metodă next(). De asemenea, trebuie să salvați manual starea. De multe ori, devine foarte greu de făcut acest lucru. Deoarece generatoarele sunt, de asemenea, iterabile, acestea pot fi utilizate pentru a implementa iterabile fără codul suplimentar de tip boilerplate. Să vedem un exemplu simplu.

Problemă: Vrem să facem un iterabil personalizat care să returneze This, is și iterable.. Iată o implementare folosind iteratori –

const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.

Iată același lucru folosind generatoare –

function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.

Puteți compara ambele versiuni. Este adevărat că acesta este un exemplu oarecum artificial. Dar ilustrează punctele –

  • Nu trebuie să ne facem griji cu privire la Symbol.iterator
  • Nu trebuie să implementăm next().
  • Nu trebuie să realizăm manual obiectul de întoarcere al lui next(), adică { value: 'This', done: false }.
  • Nu trebuie să salvăm starea. În exemplul iteratorului, starea a fost salvată în variabila step. Valoarea acesteia a definit ceea ce a ieșit din iterabil. Nu a trebuit să facem nimic de acest fel în generator.

Funcționalitate asincronă mai bună

Codul care utilizează promisiuni și callback-uri, cum ar fi –

function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}

poate fi scris ca (cu ajutorul unor biblioteci precum co.js)-

const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});

Câțiva cititori au observat că este paralelă cu utilizarea lui async/await. Aceasta nu este o coincidență. async/await poate urma o strategie similară și înlocuiește randamentul cu await în cazurile în care sunt implicate promisiuni. Se poate baza pe generatoare. Vedeți acest comentariu pentru mai multe informații.

Curente de date infinite

Este posibil să se creeze generatoare care nu se termină niciodată. Luați în considerare acest exemplu –

function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2

Creăm un generator naturalNumbers. În interiorul funcției, avem o buclă infinită while. În acea buclă, yield îl num. Când generatorul cedează, acesta este suspendat. Când apelăm din nou next(), generatorul se trezește, continuă de unde a fost suspendat (în acest caz yield num) și se execută până când se întâlnește un alt yield sau generatorul se termină. Deoarece următoarea instrucțiune este num = num + 1, aceasta actualizează num. Apoi, se merge în partea de sus a buclei while. Condiția este încă adevărată. Se întâlnește următoarea linie yield num. Acesta produce num actualizat și se suspendă. Acest lucru continuă atâta timp cât doriți.

Generatoarele ca observatori

Generatoarele pot, de asemenea, să primească valori folosind funcția next(val). Atunci generatorul se numește observator, deoarece se trezește atunci când primește valori noi. Într-un anumit sens, el continuă să observe pentru valori și acționează atunci când primește una. Puteți citi mai multe despre acest model aici.

Avantajele generatoarelor

După cum s-a văzut în exemplul Infinite Data Streams, acesta este posibil doar datorită evaluării leneșe. Evaluarea leneșă este un model de evaluare care amână evaluarea unei expresii până când este nevoie de valoarea sa. Adică, dacă nu avem nevoie de valoare, aceasta nu va exista. Ea este calculată pe măsură ce o solicităm. Să vedem un exemplu –

function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}

La powerSeries se dă seria numărului ridicat la o putere. De exemplu, seria de puteri a lui 3 ridicată la 2 ar fi 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Când facem const powersOf2 = powerSeries(3, 2); nu facem decât să creăm obiectul generator. Niciuna dintre valori nu a fost calculată. Acum, dacă apelăm next(), 9 va fi calculat și retuned.

Eficientă din punct de vedere al memoriei

O consecință directă a evaluării leneșe este că generatoarele sunt eficiente din punct de vedere al memoriei. Generăm doar valorile care sunt necesare. Cu funcțiile normale, trebuia să generăm în prealabil toate valorile și să le păstrăm la îndemână în cazul în care le folosim mai târziu. Cu toate acestea, cu generatoarele, putem amâna calculul până când avem nevoie de el.

Putem crea funcții combinatoare pentru a acționa asupra generatoarelor. Combinatorii sunt funcții care combină iterabilele existente pentru a crea altele noi. un astfel de combinator este take. Acesta ia primele n elemente ale unui iterabil. Iată o implementare –

function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}

Iată câteva cazuri interesante de utilizare a lui take

take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49

Iată o implementare a bibliotecii ciclate (fără funcționalitatea de inversare).

function * cycled(iter) {
const arrOfValues =
while (true) {
for (const val of arrOfValues) {
yield val
}
}
}console.log(...take(10, cycled(take(3, naturalNumbers()))))// 1 2 3 1 2 3 1 2 3 1

Caveți

Există câteva puncte pe care ar trebui să le rețineți în timpul programării cu ajutorul generatoarelor.

  • Obiectele generatoare sunt accesate doar o singură dată. Odată ce ați epuizat toate valorile, nu mai puteți itera din nou peste ele. Pentru a genera din nou valorile, trebuie să creați un nou obiect generator.
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
  • Obiectele generator nu permit accesul aleatoriu, așa cum este posibil cu array-urile. Deoarece valorile sunt generate una câte una, accesarea unei valori aleatoare ar duce la calcularea valorilor până la elementul respectiv. Prin urmare, nu este vorba de acces aleatoriu.

Concluzie

În generatoare sunt încă multe lucruri de acoperit. Lucruri precum yield *, return() și throw(). Generatoarele fac, de asemenea, posibile corutinele. Am enumerat câteva referințe pe care le puteți citi pentru a înțelege mai bine generatoarele.

Vă puteți îndrepta către pagina itertools a lui Python și puteți vedea câteva dintre utilitățile care permit lucrul cu iteratori și generatoare. Ca un exercițiu, puteți implementa singuri utilitățile.

.

admin

Lasă un răspuns

Adresa ta de email nu va fi publicată.

lg