Apr 23, 2018 – 10 min read

ES6 introducerade ett nytt sätt att arbeta med funktioner och iteratorer i form av generatorer (eller generatorfunktioner). En generator är en funktion som kan stanna mitt i vägen och sedan fortsätta där den slutade. Kort sagt, en generator ser ut att vara en funktion men beter sig som en iterator.

Fun Fact: async/await kan baseras på generatorer. Läs mer här.

Generatorer är intrikat kopplade till iteratorer. Om du inte känner till iteratorer finns här en artikel för att bättre förstå dem.

Här är en enkel analogi för att få en känsla för generatorer innan vi fortsätter med de tekniska detaljerna.

Föreställ dig att du läser en nagelbitande technothriller. Du är helt uppslukad av bokens sidor och hör knappt din dörrklocka ringa. Det är pizzabudet. Du reser dig upp för att öppna dörren. Men innan du gör det sätter du ett bokmärke på den sista sidan du läste. Du sparar mentalt händelserna i handlingen. Sedan går du och hämtar din pizza. När du återvänder till ditt rum börjar du boken från den sida som du satte bokmärket på. Du börjar inte från första sidan igen. På sätt och vis agerade du som en generatorfunktion.

Låt oss se hur vi kan använda generatorer för att lösa några vanliga problem när vi programmerar. Men innan dess ska vi definiera vad generatorer är.

Vad är generatorer?

En normal funktion som den här kan inte stoppas innan den har avslutat sin uppgift, dvs. dess sista rad är utförd. Den följer något som kallas run-to-completion-modellen.

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

Det enda sättet att avsluta normalFunc är genom att returngå från den eller throwge ett fel. Om du anropar funktionen igen kommer den att börja exekveringen från början igen.

En generator är däremot en funktion som kan stanna mitt i vägen och sedan fortsätta från där den stannade.

Här är några andra vanliga definitioner av generatorer –

  • Generatorer är en speciell klass av funktioner som förenklar arbetet med att skriva iteratorer.
  • En generator är en funktion som producerar en sekvens av resultat i stället för ett enda värde, dvs. du genererar en serie värden.

I JavaScript är en generator en funktion som returnerar ett objekt som du kan kalla next() på. Varje anrop av next() returnerar ett objekt med formen –

{ 
value: Any,
done: true|false
}

Gegenskapen value innehåller värdet. Egenskapen done är antingen true eller false. När done blir true stannar generatorn och kommer inte att generera några fler värden.

Här är en illustration av samma sak –

Normala funktioner vs generatorer

Bemärk den streckade pilen som stänger avkastningsresumtions-avkastnings-slingan strax före Finish i Generators del av bilden. Det finns en möjlighet att en generator aldrig avslutas. Vi kommer att se ett exempel senare.

Skapa en generator

Vi ska se hur vi kan skapa en generator i 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

Fokusera på de fetstilade delarna. För att skapa en generatorfunktion använder vi function * syntax istället för bara function. Det kan finnas valfritt antal mellanslag mellan nyckelordet function, * och funktionsnamnet. Eftersom det bara är en funktion kan du använda den överallt där en funktion kan användas, dvs. inuti objekt och klassmetoder.

Inuti funktionskroppen har vi inget return. Istället har vi ett annat nyckelord yield (rad 2). Det är en operatör med vilken en generator kan pausa sig själv. Varje gång en generator möter ett yield ”returnerar” den det värde som anges efter det. I det här fallet returneras Hello,. Vi säger dock inte ”returneras” i samband med generatorer. Vi säger ”generatorn har gett Hello, ”.

Vi kan också returnera från en generator. Men return sätter egenskapen done till true varefter generatorn inte kan generera fler värden.

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

I linje 3 skapar vi generatorobjektet. Det verkar som om vi åberopar funktionen generatorFunction. Det gör vi faktiskt! Skillnaden är att i stället för att returnera något värde returnerar en generatorfunktion alltid ett generatorobjekt. Generatorobjektet är en iterator. Du kan alltså använda det i for-of-slingor eller andra funktioner som accepterar en iterabel.

I rad 4 anropar vi next()-metoden på generatorObject. Med detta anrop börjar generatorn att exekvera. Först console.log den This will be executed first. Därefter möter den en yield 'Hello, '. Generatorn ger värdet som ett objekt { value: 'Hello, ', done: false } och avbryter/pausar. Nu väntar den på nästa anrop.

I linje 5 anropar vi next() igen. Den här gången vaknar generatorn upp och börjar exekvera från där den slutade. Nästa rad som den hittar är en console.log. Den loggar strängen I will be printed after the pause. Ytterligare en yield påträffas. Värdet ges ut som objektet { value: 'World!', done: false }. Vi extraherar egenskapen value och loggar den. Generatorn sover igen.

I rad 6 åberopar vi återigen next(). Den här gången finns det inga fler rader att utföra. Kom ihåg att varje funktion implicit returnerar undefined om inget return statement anges. Därför returnerar generatorn (i stället för att ge) ett objekt { value: undefined, done: true}. done sätts till true. Detta signalerar slutet på denna generator. Nu kan den inte generera fler värden eller återuppta igen eftersom det inte finns några fler uttalanden som ska exekveras.

Vi måste göra ett nytt annat generatorobjekt för att köra generatorn igen.

Användningsområden för generatorer

Det finns många häftiga användningsområden för generatorer. Låt oss se några av dem.

Implementering av Iterables

När du implementerar en iterator måste du manuellt skapa ett iteratorobjekt med en next()-metod. Dessutom måste du manuellt spara tillståndet. Ofta blir det riktigt svårt att göra det. Eftersom generatorer också är iterables kan de användas för att implementera iterables utan den extra boilerplate-koden. Låt oss se ett enkelt exempel.

Problem: Vi vill göra en anpassad iterabel som returnerar This, is och iterable.. Här är en implementering som använder iteratorer –

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.

Här är samma sak som använder generatorer –

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

Du kan jämföra båda versionerna. Det är sant att detta är ett lite väl konstruerat exempel. Men det illustrerar poängen –

  • Vi behöver inte oroa oss för Symbol.iterator
  • Vi behöver inte implementera next().
  • Vi behöver inte manuellt skapa returobjektet för next() dvs. { value: 'This', done: false }.
  • Vi behöver inte spara tillståndet. I iteratorns exempel sparades tillståndet i variabeln step. Dess värde definierade vad som skrevs ut från iterabeln. Vi behövde inte göra något sådant i generatorn.

Bättre asynkron funktionalitet

Kod som använder löften och callbacks som –

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

kan skrivas som (med hjälp av bibliotek som 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}`);
}
});

En del läsare har kanske lagt märke till att det finns en parallell till användningen av async/await. Det är ingen tillfällighet. async/await kan följa en liknande strategi och ersätter yield med await i fall där löften är inblandade. Det kan baseras på generatorer. Se den här kommentaren för mer information.

Oändliga dataströmmar

Det är möjligt att skapa generatorer som aldrig tar slut. Tänk på det här exemplet –

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

Vi gör en generator naturalNumbers. Inne i funktionen har vi en oändlig while-slinga. I den slingan yield vi num den num. När generatorn ger efter, upphävs den. När vi anropar next() igen vaknar generatorn upp, fortsätter från den plats där den avbröts (i det här fallet yield num) och körs tills en annan yield anträffas eller tills generatorn slutar. Eftersom nästa uttalande är num = num + 1 uppdaterar det num. Därefter går den till början av while-slingan. Villkoret är fortfarande sant. Den stöter på nästa rad yield num. Den ger den uppdaterade num och avbryter. Detta fortsätter så länge du vill.

Generatorer som observatörer

Generatorer kan också ta emot värden med hjälp av funktionen next(val). Då kallas generatorn för en observatör eftersom den vaknar upp när den tar emot nya värden. På sätt och vis fortsätter den att observera efter värden och agerar när den får ett. Du kan läsa mer om det här mönstret här.

Fördelar med generatorer

Som framgår av exemplet Infinite Data Streams är det möjligt endast på grund av lazy evaluation. Lazy Evaluation är en utvärderingsmodell som fördröjer utvärderingen av ett uttryck tills dess att dess värde behövs. Det vill säga, om vi inte behöver värdet kommer det inte att existera. Det beräknas när vi kräver det. Låt oss se ett exempel –

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

Den powerSeries ger serien av talet upphöjt till en potens. Exempelvis skulle potensserien för 3 upphöjt till 2 vara 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). När vi gör const powersOf2 = powerSeries(3, 2); skapar vi bara generatorobjektet. Inget av värdena har beräknats. Om vi nu anropar next() skulle 9 beräknas och återkopplas.

Minneeffektivt

En direkt konsekvens av Lazy Evaluation är att generatorer är minneseffektiva. Vi genererar bara de värden som behövs. Med normala funktioner behövde vi generera alla värden i förväg och ha dem kvar om vi skulle använda dem senare. Men med generatorer kan vi skjuta upp beräkningen tills vi behöver den.

Vi kan skapa kombinatorfunktioner för att agera på generatorer. Kombinatorer är funktioner som kombinerar befintliga iterabler för att skapa nya. take är en sådan kombinator. Den tar de första n elementen i en iterabel. Här är en implementering –

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

Här är några intressanta användningsområden för take

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

Här är en implementering av cyklat bibliotek (utan omvänd funktionalitet).

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

Caveats

Det finns några punkter som du bör komma ihåg när du programmerar med hjälp av generatorer.

  • Generatorobjekten har endast engångsåtkomst. När du har uttömt alla värden kan du inte iterera över dem igen. För att generera värdena igen måste du göra ett nytt generatorobjekt.
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
  • Generatorobjekt tillåter inte slumpmässig åtkomst som är möjligt med matriser. Eftersom värdena genereras ett efter ett skulle åtkomst till ett slumpmässigt värde leda till beräkning av värden fram till det elementet. Därför är det inte slumpmässig åtkomst.

Slutsats

Det är mycket som ännu inte har behandlats i generatorer. Saker som yield *, return() och throw(). Generatorer gör också koroutiner möjliga. Jag har listat några referenser som du kan läsa för att få ytterligare förståelse för generatorer.

Du kan gå över till Pythons itertools-sida och se några av de verktyg som gör det möjligt att arbeta med iteratorer och generatorer. Som en övning kan du implementera verktygen själv.

admin

Lämna ett svar

Din e-postadress kommer inte publiceras.

lg