Apr 23, 2018 – 10 min read

ES6 ha introdotto un nuovo modo di lavorare con funzioni e iteratori sotto forma di Generatori (o funzioni generatrici). Un generatore è una funzione che può fermarsi a metà strada e poi continuare da dove si è fermata. In breve, un generatore sembra essere una funzione ma si comporta come un iteratore.

Fatto divertente: async/await può essere basato su generatori. Leggete di più qui.

I generatori sono intrinsecamente legati agli iteratori. Se non conosci gli iteratori, ecco un articolo per migliorarne la comprensione.

Ecco una semplice analogia per avere un’intuizione dei generatori prima di procedere con i dettagli tecnici.

Immagina di leggere un techno-thriller avvincente. Tutto assorto nelle pagine del libro, sentite a malapena suonare il campanello. È il fattorino della pizza. Vi alzate per aprire la porta. Tuttavia, prima di farlo, metti un segnalibro all’ultima pagina che hai letto. Salvi mentalmente gli eventi della trama. Poi, vai a prendere la pizza. Quando torni nella tua stanza, inizi il libro dalla pagina in cui hai messo il segnalibro. Non ricominci dalla prima pagina. In un certo senso, hai agito come una funzione generatrice.

Vediamo come possiamo utilizzare i generatori per risolvere alcuni problemi comuni durante la programmazione. Ma prima di questo, definiamo cosa sono i generatori.

Cosa sono i generatori?

Una funzione normale come questa non può essere fermata prima che finisca il suo compito, cioè la sua ultima linea viene eseguita. Segue qualcosa chiamato modello run-to-completion.

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

L’unico modo per uscire dalla normalFunc è returndalla funzione, o throwda un errore. Se chiamate di nuovo la funzione, essa ricomincerà l’esecuzione dall’inizio.

Al contrario, un generatore è una funzione che può fermarsi a metà strada e poi continuare da dove si è fermata.

Ecco alcune altre definizioni comuni di generatori –

  • I generatori sono una classe speciale di funzioni che semplificano il compito di scrivere iteratori.
  • Un generatore è una funzione che produce una sequenza di risultati invece di un singolo valore, cioè si genera una serie di valori.

In JavaScript, un generatore è una funzione che restituisce un oggetto su cui è possibile chiamare next(). Ogni invocazione di next() restituirà un oggetto di forma –

{ 
value: Any,
done: true|false
}

La proprietà value conterrà il valore. La proprietà done è true o false. Quando la done diventa true, il generatore si ferma e non genera più alcun valore.

Ecco un’illustrazione dello stesso –

Funzioni Normali vs Generatori

Nota la freccia tratteggiata che chiude il ciclo rendimento-resume-rendimento appena prima di Fine nella parte Generatori dell’immagine. C’è la possibilità che un generatore non finisca mai. Vedremo un esempio più tardi.

Creazione di un generatore

Vediamo come possiamo creare un generatore in 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

Focalizzatevi sulle parti in grassetto. Per creare una funzione generatrice, usiamo la sintassi function * invece che solo function. Qualsiasi numero di spazi può esistere tra la parola chiave function, il * e il nome della funzione. Dal momento che è solo una funzione, potete usarla ovunque possa essere usata una funzione, cioè dentro oggetti e metodi di classe.

All’interno del corpo della funzione, non abbiamo un return. Invece, abbiamo un’altra parola chiave yield (linea 2). È un operatore con cui un generatore può mettere in pausa se stesso. Ogni volta che un generatore incontra un yield, “restituisce” il valore specificato dopo di esso. In questo caso, viene restituito Hello,. Tuttavia, non diciamo “restituito” nel contesto dei generatori. Diciamo “il generatore ha prodotto Hello, “.

Possiamo anche tornare da un generatore. Tuttavia, return imposta la proprietà done a true dopo di che il generatore non può generare altri valori.

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

Nella linea 3, creiamo l’oggetto generatore. Sembra che stiamo invocando la funzione generatorFunction. Infatti è così! La differenza è che invece di restituire qualsiasi valore, una funzione generatore restituisce sempre un oggetto generatore. L’oggetto generatore è un iteratore. Quindi potete usarlo nei cicli for-of o in altre funzioni che accettano un iterabile.

Nella linea 4, chiamiamo il metodo next() sul generatorObject. Con questa chiamata, il generatore inizia l’esecuzione. Prima, console.log il This will be executed first. Poi, incontra un yield 'Hello, '. Il generatore restituisce il valore come oggetto { value: 'Hello, ', done: false } e sospende/pausa. Ora, è in attesa della prossima invocazione.

Nella linea 5, chiamiamo di nuovo next(). Questa volta il generatore si sveglia e inizia l’esecuzione da dove aveva lasciato. La prossima linea che trova è una console.log. Registra la stringa I will be printed after the pause. Viene incontrato un altro yield. Il valore è reso come l’oggetto { value: 'World!', done: false }. Estraiamo la proprietà value e la registriamo. Il generatore dorme di nuovo.

Nella linea 6, invochiamo di nuovo next(). Questa volta non ci sono più linee da eseguire. Ricordate che ogni funzione restituisce implicitamente undefined se non viene fornita una dichiarazione di ritorno. Quindi, il generatore restituisce (invece di restituire) un oggetto { value: undefined, done: true}. Il done è impostato a true. Questo segnala la fine di questo generatore. Ora, non può generare altri valori o riprendere di nuovo poiché non ci sono più istruzioni da eseguire.

Abbiamo bisogno di creare un altro oggetto generatore per eseguire di nuovo il generatore.

Usi dei generatori

Ci sono molti fantastici casi d’uso dei generatori. Vediamone alcuni.

Implementazione degli iterabili

Quando implementate un iteratore, dovete creare manualmente un oggetto iteratore con un metodo next(). Inoltre, devi salvare manualmente lo stato. Spesso, diventa davvero difficile farlo. Poiché i generatori sono anche iterabili, possono essere usati per implementare gli iterabili senza il codice extra di boilerplate. Vediamo un semplice esempio.

Problema: Vogliamo fare un iterabile personalizzato che restituisca This, is e iterable.. Ecco un’implementazione usando gli 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.

Ecco la stessa cosa usando i generatori –

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

Puoi confrontare entrambe le versioni. È vero che questo è un esempio un po’ artificioso. Ma illustra i punti –

  • Non dobbiamo preoccuparci di Symbol.iterator
  • Non dobbiamo implementare next().
  • Non dobbiamo fare manualmente l’oggetto di ritorno di next() cioè { value: 'This', done: false }.
  • Non dobbiamo salvare lo stato. Nell’esempio dell’iteratore, lo stato è stato salvato nella variabile step. Il suo valore definiva ciò che veniva emesso dall’iterabile. Non abbiamo dovuto fare nulla del genere nel generatore.

Migliore funzionalità asincrona

Il codice che usa promesse e callback come –

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

può essere scritto come (con l’aiuto di librerie come 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}`);
}
});

Alcuni lettori potrebbero aver notato che è parallelo all’uso di async/await. Questa non è una coincidenza. async/await può seguire una strategia simile e sostituisce il rendimento con await nei casi in cui sono coinvolte promesse. Può essere basato su generatori. Vedi questo commento per maggiori informazioni.

Flussi di dati infiniti

È possibile creare generatori che non finiscono mai. Considerate questo esempio –

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

Facciamo un generatore naturalNumbers. All’interno della funzione, abbiamo un ciclo infinito while. In questo ciclo, facciamo yield il num. Quando il generatore cede, viene sospeso. Quando chiamiamo di nuovo next(), il generatore si sveglia, continua da dove era stato sospeso (in questo caso yield num) ed esegue fino a quando si incontra un altro yield o il generatore finisce. Poiché la prossima dichiarazione è num = num + 1, aggiorna num. Poi, va all’inizio del ciclo while. La condizione è ancora vera. Incontra la prossima linea yield num. Cede il num aggiornato e sospende. Questo continua finché volete.

Generatori come osservatori

I generatori possono anche ricevere valori usando la funzione next(val). Allora il generatore è chiamato osservatore, poiché si sveglia quando riceve nuovi valori. In un certo senso, continua ad osservare i valori e agisce quando ne riceve uno. Puoi leggere di più su questo modello qui.

Svantaggi dei generatori

Come visto nell’esempio di Infinite Data Streams, è possibile solo grazie alla valutazione pigra. La valutazione pigra è un modello di valutazione che ritarda la valutazione di un’espressione finché il suo valore non è necessario. Cioè, se non abbiamo bisogno del valore, non esiste. Viene calcolato man mano che lo richiediamo. Vediamo un esempio –

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

Il powerSeries dà la serie del numero elevato ad una potenza. Per esempio, la serie di potenza di 3 elevato a 2 sarebbe 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Quando facciamo const powersOf2 = powerSeries(3, 2); creiamo solo l’oggetto generatore. Nessuno dei valori è stato calcolato. Ora, se chiamiamo next(), 9 verrebbe calcolato e restituito.

Efficiente in termini di memoria

Una conseguenza diretta della Lazy Evaluation è che i generatori sono efficienti in termini di memoria. Generiamo solo i valori che sono necessari. Con le funzioni normali, avevamo bisogno di pre-generare tutti i valori e tenerli in giro nel caso li avessimo usati in seguito. Tuttavia, con i generatori, possiamo rimandare il calcolo finché non ne abbiamo bisogno.

Possiamo creare funzioni combinatorie per agire sui generatori. I combinatori sono funzioni che combinano iterabili esistenti per crearne di nuovi.Uno di questi combinatori è take. Prende i primi nelementi di un iterabile. Ecco un’implementazione –

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

Ecco alcuni casi d’uso interessanti di take

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

Ecco un’implementazione di libreria ciclica (senza la funzionalità di inversione).

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

Caveat

Ci sono alcuni punti che dovresti ricordare mentre programmi usando i generatori.

  • Gli oggetti generatore sono solo ad accesso unico. Una volta che avete esaurito tutti i valori, non potete iterare di nuovo. Per generare nuovamente i valori, è necessario creare un nuovo oggetto generatore.
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
  • Gli oggetti generatori non permettono un accesso casuale come quello possibile con gli array. Poiché i valori sono generati uno per uno, l’accesso a un valore casuale porterebbe al calcolo dei valori fino a quell’elemento. Quindi, non è un accesso casuale.

Conclusione

Molte cose devono ancora essere coperte nei generatori. Cose come yield *, return() e throw(). I generatori rendono anche possibili le coroutine. Ho elencato alcuni riferimenti che potete leggere per comprendere meglio i generatori.

Potete andare alla pagina itertools di Python, e vedere alcune delle utility che permettono di lavorare con iteratori e generatori. Come esercizio, potete implementare voi stessi le utilità.

Sì.

admin

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

lg