Pair programming in Javascript: I generatori

P

GENERATORI SINCRONI

Questo articolo fa parte di una serie di articoli:

  1. Gli Iteratori sincroni
  2. I Generatori sincroni
  3. Gli Iteratori asincroni
  4. I Generatori asincroni

 

PAIR PROGRAMMING CON FABIO BIONDI

Siamo tornati più carichi che mai riproponendo la chiaccherata tra dev con il carissimo Fabio Biondi! Sicuramente troverete meno errori grammaticali e di pronunce inglesi rispetto al precedente appuntamento. Ve lo prometto.

 

INTRODUZIONE

I generatori sono un tipo di funzione molto particolare. Siamo abituati, quando una funzione viene invocata, ad aspettare il suo completamento prima di poter proseguire con il programma.
I generatori rompono questa logica, permettendoci di sospendere l’esecuzione di una funzione in più punti e di passare informazioni in entrambe le direzioni.
Sebbene siano un tipo di funzione poco conosciuto e spesso frainteso, permettono di implementare più agilmente la concorrenza tra task. La loro struttura li rende ottimi candidati come producer di valori nonché come gestori di code di funzioni.

Altri esempi che mostrano cosa è possibile fare con i generatori potrete trovarli ai seguenti link:
the-hidden-power-of-es6-generators-observable-async-flow-control
javascript-generators-understanding-sample-use-cases

Infine, l’unione tra generatori e Promise ha permesso la creazione di uno dei pattern più potenti del Javascript: l’async/await. Su questo aspetto in particolare abbiamo speso più di due parole nel video che vi consiglio di visionare anche se state leggendo l’articolo.


Nel video mostro un codice precedentemente preparato che potete testare seguendo questo link.

 

I GENERATORI

La dichiarazione di un generatore è molto simile a quella di una funzione, con l’aggiunta di un asterisco tra la keyword function e il nome della funzione:

function * generator(par1, par2, …, parN) { 
  yield 42; 
}

Possono esistere generatori anonimi ma non è possibile usare la fat arrow syntax.
Essendo comunque una funzione può definire uno o più parametri e alla fine restituisce sempre un valore che, come le funzioni normali, è undefined se non specificato. Anche l’invocazione è tale e quale a quella delle funzioni:

generator(arg1, arg2, …, argN);

In aggiunta è possibile utilizzare una nuova keyword, ovvero yield, che restituisce il controllo al chiamante sospendendo l’esecuzione del generatore. Questa keyword apre una strada a doppio senso, sarà possibile non solo restituire un valore grazie a questa keyword ma anche introdurne uno all’interno del generatore stesso quando decideremo di riprenderne l’esecuzione.
Data la natura “stop & go” dei generatori non è difficile capire perché sono stati scelti gli iteratori per il loro controllo.

Vediamo un esempio per prendere confidenza con l’argomento:

function* numbers() {
  yield 1;
  yield 3;
  yield 5;
  yield 7;
  return 9;
}

Invochiamo quindi il generatore per ottenere un iteratore:

const iter = numbers();

Dopodiché eseguiamo un’unica iterazione:

iter.next();

Il generatore eseguirà il codice presente all’interno di esso finché non incontrerà la keyword yield, dove si arresterà in automatico. Dato che il valore numerico 1 è associato alla prima yield sarà esso a popolare la proprietà value dell’oggetto ritornato dall’iteratore:

// { value:1, done:false }

Per far riprendere l’esecuzione del generatore dobbiamo agire nuovamente sull’iteratore, chiamando nuovamente il metodo next. Questa volta ci aspettiamo il valore numerico 3 nella proprietà value:

iter.next(); // { value:3, done:false }

Adesso consumiamo in toto l’iteratore e spendiamo due parole sui valori che riceviamo:

iter.next(); // { value:5, done:false }
iter.next(); // { value:7, done:false }
iter.next(); // { value:9, done:true }
iter.next(); // { value:undefined, done:true }

L’ultima coppia undefined-true non dovrebbe sorprenderci dato che abbiamo visto che un iteratore, quando supera i limiti della sua iterazione, ritorna sempre un oggetto contenente undefined come valore per la proprietà value e true come valore per la proprietà done.
Quello che può risultarci strano è il valore 9 associato con un done true. Quando abbiamo trattato gli iteratori abbiamo visto come l’ultimo valore valido deve essere comunque associato ad un done false. Perché quindi il 9 è in coppia con un true? Perché esso non viene restituito da uno yield ma dal return finale.
Se infatti avessimo utilizzato l’iteratore con un ciclo for..of, non avremmo visualizzato il numero 9 finale:

const iter = numbers();
for (const v of iter) {
  console.log(v); // 1 3 5 7 
}

Ricordiamo che gli iteratori forniti dal javascript sono iterabili a loro volta, ecco perché al for of abbiamo dato in pasto l’iteratore. È interessante notare come i generatori, nonostante forniscano iteratori quando vengono invocati, non sono a loro volta iterabili. Ecco che il seguente ciclo produrrà un errore:

for (const v of number) {
  console.log(v); // TypeEror: number is not iterable
}

Mentre questo no, dato che è presente l’invocazione del generatore che produce un iteratore:

for (const v of number()) {
  console.log(v); // 1 3 5 7
}

 

È importante sapere che ogni volta che un iteratore viene creato da un generatore, viene implicitamente creata una istanza del generatore che quello specifico iteratore controllerà. In nessun modo più istanze di un medesimo generatore si influenzano tra loro implicitamente:

const iter1 = numbers();
const iter2 = numbers();

iter1.next(); // { value:1, done:false } 
iter1.next(); // { value:3, done:false } 
iter1.next(); // { value:5, done:false } 

// iter2 controlla un'altra istanza del generatore 
// indipendente sotto ogni aspetto da quella gestita da iter1
iter2.next(); // { value:1, done:false }

 

Ora aumentiamo un poco la complessità per capire in che modo la keyword yield ci permette di inserire un valore all’interno del generatore ogni volta che esso è in pausa:

function* test(a, b, c) {
  yield a;
  yield b;
  const d = yield c;
  yield d;
}

// l’istanza del generatore viene inizializzata ma non eseguita 
const testIterator = test(1, 2, 3);
testIterator.next(); // { done:false, value: 1} 
testIterator.next(); // { done:false, value: 2} 
testIterator.next(); // { done:false, value: 3} 

// il valore passato grazie a next() verrà inserito al posto del terzo yield 
// esattamente dove l’esecuzione era stata sospesa 

testIterator.next(4); // { done: false, value: 4} 
testIterator.next(); // { done:true, value: undefined}

Il generatore restituisce uno alla volta i tre argomenti che gli abbiamo passato quando lo abbiamo utilizzato. Quando l’esecuzione è sospesa sul terzo yield, il quale ci ha restituito il valore del parametro c, possiamo introdurre un valore all’interno del generatore grazie al metodo next(). Questo valore inizializzerà la costante d che poi ci verrà restituita dal quarto yield.

Ovviamente questa trattazione dei generatori è tutt’altro che esaustiva, ma ci permette già di sfruttarli per semplificare notevolmente la creazione di un iteratore sia per la collezione degli users che per il fibonacciProducer, entrambi visti nel precedente articolo.

 

da iteratori a generatori

Iniziamo dalla collezione degli users dove possiamo riscrivere il metodo [Symbol.iterator]() con una funzione generatore:

*[Symbol.iterator]() {
  for (const property in this) {
    if (this[property]) yield property;
  }
}

Niente male vero? Se l’ennesima ‘property’ ha come valore ‘true’ possiamo restituirla grazie a yield e mettere in pausa l’istanza del generatore. Altrimenti è ovvio che il ciclo rimane in esecuzione mentre il costrutto if scarta le property ‘false’.

Adesso occupiamoci del fibonacciProducer che era messo male pure lui:

return function* iteratorGenerator() {
  while (true) {
    const current = nextFibonacciNumber();
    if (current > Number.MAX_SAFE_INTEGER) return;
    yield current;
  }
}();

Mentre di solito un ciclo infinito all’interno di una funzione non è una grande idea, nel caso di un generatore non presenta particolari problemi, purché si sfrutti dovutamente la keyword yield. Quello che avviene in questo generatore è semplice: viene richiesto un nuovo valore al producer che prima di essere restituito viene controllato per determinare se è il momento di terminare l’istanza del generatore.

Considerando che l’interfaccia Iterator presenta qualche piccola particolarità in più di quelle considerate precedentemente e che è importante che un Iterator sia anche un Iterable, è consigliato utilizzare i generatori anziché implementare le tre interfacce manualmente. Infatti uno degli obiettivi dei generatori è proprio quello di aiutarci ad costruire iteratori.

A proposito di me

Andrea Simone Costa

Classe 1997, Toscano DOCP, Asimov oriented.
Si è diplomato come perito elettronico, ma ha ben presto tradito le origini per immergersi nell'universo JavaScript. Ama condividere ciò che ha imparato in modo semplice e pragmatico, senza mai ricadere nel banale, coltivando segretamente il sogno di insegnare.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti