Le funzioni ritornano normalmente un solo valore (a volte non ritornano nulla).
I generatori possono ritornare (âyieldâ) valori multipli, uno dopo lâaltro, ogni volta che vengono invocati. Sono, di fatto, lo strumento ideale da utilizzare con gli iteratori, dal momento che ci consentono di creare flussi di dati con facilità .
Le funzioni generatrici
Per creare un generatore, abbiamo bisogno di uno specifico costrutto sintattico: function*, chiamato, appunto, âfunzione generatriceâ.
Ecco un esempio:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Le funzioni generatrici si comportano diversamente rispetto alle normali funzioni. Quando una generatrice viene invocata, di fatto, il codice al suo interno non viene eseguito, ma ritorna uno speciale oggetto, chiamato âoggetto generatoreâ, che ne consente di gestire lâesecuzione.
Dai unâocchiata qua:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "la funzione generatrice" crea un "oggetto generatore"
let generator = generateSequence();
alert(generator); // [object Generator]
Lâesecuzione del codice della funzione non è ancora iniziata:
Il metodo principale di un oggetto generatore è next(). Quando invocato, esegue le istruzioni in esso contenute fino alla prossima istruzione yield <valore> (valore può essere omesso, in tal caso sarà undefined). A questo punto lâesecuzione si arresta e il valore viene âcedutoâ al codice esterno.
Il risultato dellâesecuzione di next() è sempre un oggetto con due proprietà :
value: il valore che viene âcedutoâ.done:true, se il codice della funzione è stato eseguito completamente, altrimenti,false.
Ad esempio, qui andiamo a creare un generatore e otteniamo il primo valore âcedutoâ:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
A questo punto, abbiamo ottenuto solo il primo valore e lâesecuzione della funzione ha raggiunto la seconda riga:
Invochiamo ancora generator.next(). Lâesecuzione del codice riprenderà da dove si era fermata, fino a restituire il valore del prossimo yield:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
Per finire, invocandolo nuovamente (generator.next()), si raggiungerà lâistruzione return che terminerà la funzione:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
A questo punto il generatore ha terminato. Possiamo vederlo dal risultato finale done:true e value:3.
Effettuare nuove chiamate a generator.next() non avrebbe più senso. Se lo facciamo, otterremmo sempre lo stesso oggetto: {done: true}.
function* f(â¦)orfunction *f(â¦)?Entrambe le sintassi sono corrette.
La prima, tuttavia, è la più utilizzata, dal momento che è lâasterisco * a indicare che la funzione è una generatrice, non il nome. Per questo motivo ha più senso accoppiare lâasterisco con la parola chiave function.
I generatori sono iteratori
Come probabilmente avrai intuito dalla presenza del metodo next(), i generatori sono iterabili.
Possiamo eseguire cicli sui valori ritornati utilizzando for..of:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, then 2
}
Non trovi che sia molto più leggibile di .next().value?
â¦Nota bene: lâesempio precedente produrrà come risultato 1 e 2, nulla più. Il 3 non verrà preso in considerazione!
La spiegazione di questo comportamento sta nel fatto che for..of ignora lâultimo value non appena la proprietà è done: true. Per questo motivo, se vogliamo che tutti i valori siano mostrati da for..of, dobbiamo ritornarli tramite yield:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, poi 2, poi 3
}
Dal momento che i generatori sono iteratori, possiamo usufruire di tutte le funzionalità che ne derivano, per esempio lo âspread operatorâ.
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
Nellâesempio precedente ...generateSequence() converte lâoggetto generatore iteratore in un array di elementi (puoi approfondire lâargomento relativo allo spread operator nel capitolo Articolo "rest-parameters-spread-operator" non trovato)
Usare i generatori con gli iteratori
Tempo fa, nel capitolo Iteratori abbiamo creato un oggetto iteratore range che ritorna valori from..to (daâ¦a).
Ecco lâesempio, per rinfrescarci la memoria:
let range = {
from: 1,
to: 5,
// for..of invoca questo metodo solo all'inizio
[Symbol.iterator]() {
// ...ritorna l'oggetto iteratore:
// da qui in poi, for..of utilizzerà solo quell'oggetto per ottenere i valori successivi
return {
current: this.from,
last: this.to,
// next() viene invocata ad ogni iterazione fino alla fine del ciclo for..of
next() {
// dovrebbe ritornare il valore sotto forma di un oggett {done:..., value:...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
};
// l'iterazione su range ritorna i numeri compresi tra range.from e range.to
alert([...range]); // 1,2,3,4,5
Possiamo usare una funzione generatrice come iteratore assegnandola a Symbol.iterator.
Ecco qui lo stesso range, ma in una forma più compatta:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() {
// forma abbreviata di [Symbol.iterator]: function*()
for (let value = this.from; value <= this.to; value++) {
yield value;
}
},
};
alert([...range]); // 1,2,3,4,5
Il funzionamento è invariato dal momento che range[Symbol.iterator]() ora ritorna un generatore che è esattamente quello che for..of si aspetta:
- ha un metodo
.next() - il quale ritorna valori nella forma
{value: ..., done: true/false}
Questa non è una coincidenza, ovviamente. I generatori sono stati aggiunti al linguaggio JavaScript tenendo a mente gli iteratori, per implementarli più facilmente.
La variante con i generatori è molto più concisa del codice originale di range ma mantiene le funzionalità invariate.
Negli esempi precedenti abbiamo generato sequenze finite ma possiamo anche creare generatori che restituiscono valori infinitamente. Per esempio, una sequenza infinita di numeri pseudo-casuali.
Ciò richiederebbe sicuramente un break (o un return) nel for..of che utilizziamo per iterare su tale generatore, altrimenti il ciclo si ripeterebbe allâinfinito bloccando lâesecuzione dellâapplicazione.
Composizione di generatori
La composizione dei generatori è una caratteristica particolare dei generatori che consente di âinnestarliâ lâuno nellâaltro, in modo trasparente.
Per esempio, data una funzione che genera una sequenza di numeri:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
Vogliamo usarla per generare una sequenza più complessa:
- per prime le cifre
0..9(con i codici carattere 48â¦57), - seguite dalle lettere dellâalfabeto
a..z(codici carattere 65â¦90) - seguite dalle lettere maiuscole
A..Z(codici carattere 97â¦122)
Possiamo usare questa sequenza, ad esempio, per creare password selezionandone i caratteri (potremmo anche aggiungere caratteri sintattici), ma generiamo la sequenza per ora.
In una normale funzione, per combinare i risultati di più funzioni, dapprima invochiamo le funzioni, ne memorizziamo i risultati e, infine, li combiniamo.
Usando i generatori, câè una sintassi speciale di yield* per âinnestareâ (comporre) un generatore allâinterno di un altro.
Il generatore composto:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
La direttiva yield* delega lâesecuzione a un altro generatore. Con il termine delega si intende che yield* gen itera sui valori del generatore gen e, in modo trasparente, inoltra i valori che restituisce verso lâesterno. Come se i valori fossero restituiti dal generatore più esterno.
Il risultato è lo stesso che otterremmo mettendo in sequenza il codice dei generatori annidati:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
La composizione dei generatori è un modo naturale di immettere il flusso di un generatore allâinterno di un altro. Non viene utilizzata memoria aggiuntiva per memorizzare i valori intermedi.
âyieldâ è una via a doppio senso
Finora i generatori sono assimilati agli iteratori, con una sintassi speciale per generare valori ma, di fatto, sono molto più potenti e flessibili.
Questo perché yield è una via a doppio senso: non solo ritorna il risultato verso lâesterno ma provvede anche a passare il valore allâinterno del generatore.
Per fare questo, dovremmo invocare generator.next(arg) con un argomento. Tale argomento diventerà il risultato di yield.
Vediamo un esempio:
function* gen() {
// Passa una domanda al codice esterno e attende una risposta
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield ritorna il valore
generator.next(4); // --> pass il risultato al generatore
- La prima invocazione di
generator.next()è sempre senza argomento. Inizia lâesecuzione e ritorna il risultato della primayield "2+2=?". A questo punto il generatore si arresta (si ferma su quella riga). - Di seguito, come mostrato nella figura sopra, il risultato di
yieldva dentro alla variabilequestionnel codice allâesterno. - Quando eseguiamo
generator.next(4), il generatore riprende lâesecuzione e4finisce nel risultato:let result = 4.
Si noti che il codice allâesterno non deve per forza invocare next(4) immediatamente. Potrebbe impiegare del tempo, ma questo non è un problema: il generatore attenderà .
Per esempio:
// ripristina il generatore dopo un certo lasso di tempo
setTimeout(() => generator.next(4), 1000);
Come possiamo vedere, a differenza delle normali funzioni, un generatore e il codice che lo invoca possono scambiarsi risultati passando valori a next/yield.
Per rendere il tutto ancora più evidente, ecco un altro esempio, con più chiamate:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?";
alert(ask2); // 9
}
let generator = gen();
alert(generator.next().value); // "2 + 2 = ?"
alert(generator.next(4).value); // "3 * 3 = ?"
alert(generator.next(9).done); // true
Illustrazione dellâesecuzione:
- La prima
.next()inizia lâesecuzione⦠raggiunge la primayield. - Il risultato viene ritornato al codice esterno.
- La seconda
.next(4)passa4al generatore come risultato della primayielde riprende lâesecuzione. - â¦la seconda
yieldviene raggiunta e diventa il risultato della chiamata al generatore. - La terza
next(9)passa9nel generatore come risultato della secondayielde riprende lâesecuzione che raggiunge la fine della funzione, dunque,done: true.
à un poâ come una partita a âping-pongâ. Ogni next(value) (escluso il primo), passa un valore al generatore. Questo valore diventa il risultato della yield corrente per poi ritornare il risultato della yield successiva.
generator.throw
Come abbiamo visto negli esempi precedenti, il codice esterno può passare un valore allâinterno del generatore, come risultato della yield.
â¦ma può anche passargli (throw) un errore. Ciò è naturale, dal momento che un errore è comunque un risultato.
Per passare un errore allâinterno di una yield, dovremmo invocare generator.throw(err). In questo caso, tale err risulta generato dalla riga contenente tale yield.
Nel prossimo esempio, la yield di "2 + 2 = ?" genera un errore:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // shows the error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
Lâerrore, lanciato allâinterno del generatore alla riga (2) genera unâeccezione alla riga (1) in corrispondenza della yield. Nellâesempio precedente try..catch gestisce lâerrore e lo mostra.
Se non gestiamo lâerrore, come accadrebbe con qualsiasi eccezione, questâultima andrebbe a causare un errore nel codice esterno.
La riga corrente del codice esterno è quella che contiene generator.throw, identificata da (2). Possiamo, pertanto, gestire lâeccezione qui, come nellâesempio:
function* generate() {
let result = yield "2 + 2 = ?"; // Errore in questa riga
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // mostra l'errore
}
Se non gestiamo lâerrore qui, come al solito, questo risalirà fino al codice più esterno, se esistente, altrimenti farà fallire lo script.
Riepilogo
- I generatori vengono creati tramite funzioni generatrici
function* f(â¦) {â¦}. - Solo nei generatori può esistere un operatore
yield. - Il codice esterno e il generatore possono interscambiare risultato tramite chiamate a
next/yield.
Nel JavaScript moderno, i generatori vengono usati raramente ma a volte possono essere utili, dal momento che la loro capacità di interscambiare dati con il codice allâesterno è alquanto unica. Sicuramente, sono un ottimo modo per creare degli iteratori.
Nel prossimo capitolo impareremo a usare i generatori asincroni, utilizzati per leggere flussi di dati generati in modo asincrono (per esempio, dati paginati ottenuti dalla rete) nei cicli for await ... of.
Questo è un caso dâuso molto importante, dal momento che nella programmazione web ci troviamo spesso a manipolare flussi di dati.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)