50 approfondimento itaratori

Nella scorsa lezione abbiamo visto come con le collezioni standard tipo gli array, sia disponibile agganciato ad una proprietà che sfrutta il symbol iterator, una funzione in grado di generare un oggetto iteratore, cioè dotato di un metodo next che a sua volta potrà essere automaticamente richiamato dal ciclo for nella forma for of, per iterare e restituire ogni elemento di quella collezione.

In questa lezione vedremo come definire, per una nostra collezione, la funzione generatrice dell’iteratore e personalizzare ciò che verrà restituito al for of se qualcuno decidesse di ciclare o di iterare sulla nostra collezione.

Per incominciare dobbiamo approfondire una caratteristica di ES6, i generators o funzioni generatrici.

Le funzioni generatrici si contraddistinguono per il fatto che davanti al suo nome un asterisco, che attribuisce un comportamento assai particolare in combinazione con l’operatore yield (genera).

Vediamo un esempio

function  * generatrice()
{
  let n=10;
  writeln(n);

  yield;

  n+=1;
  writeln(n);           
}

Quando viene invocata, una funzione generatrice restituisce un oggetto per il quale viene automaticamente attribuito il metodo next

let obj = generatrice();

La caratteristica principale della funzione generatrice è che quando viene richiamata per la prima volta, viene messa in standby, il flusso del programma ragionerebbe così

function  * generatrice()
{
  //in attesa ...

  let n=10;
  writeln(n);

  yield;

  n+=1;
  writeln(n);
}

la funzione rimane in attesa che si vada a chiamare una prima volta il metodo next sull’oggetto restituito. Proviamo a far avanzare a comando l’esecuzione della funzione

obj.next();

notiamo che il generatore procede fino ad incontrare yield, dove si blocca e rimane in attesa nuovamente

function * generatrice() { 
 let n=10; 
 writeln(n); 

 yield; 

//in attesa ... 

 n+=1; 
 writeln(n); 
}

stamperà il valore 10 e poi rimane in attesa di un altro next, da notare che non si è terminata la funzione come se avesse incontrato il return, ma sta aspettando ancora il comando successivo, per cui ordiniamo un altro next

obj.next();

il flusso riparte da dopo il yield ed incrementerà n e lo stamperà col suo nuovo valore 11. A questo punto la funzione generatrice si conclude.

Vediamo ora un caso leggermente più interessante:

function  * generatrice()
{
  let n=0;
  
  while(true)
  {
    n++;
    writeln(n);
    yield;
  }
}

Il ciclo while andrà a ciclare all’infinito l’incremento della variabile, essendo sempre TRUE, ma si avvierà ogni qualvolta invocheremo il metodo next() essendoci il comando yield all’interno del ciclo.

In poche parole, ogni volta che partirà il next(), il ciclo while incrementerà di 1 n ( n++ ), lo stamperà e si metterà in attesa. L’operatore yield, può anche restituire un valore, nel nostro caso possiamo farci passare n direttamente da yield

function  * generatrice()
{
  let n=0;
  
  while(true)
  {
    n++;
    yield n;
  }
}

classe personalizzata

andiamo a scrivere la classe miaCollezione che per praticità è un contenitore di un array ma sarebbe possibile sostituire la classe con qualsiasi struttura, contenente dati e organizzata come meglio si crede, il concetto di funzione generatrice, iteratore e collezione che diventerà iterabile non cambia.

class miaCollezione
{
  constructor () 
  {
    this.items = [];
  }
 
  add(value)
  {
    this.items.push(value)
  }
  
  * [Symbol.iterator] ()
  {
    for (let key in this.items)
    {
      if(this.items[key].length>4 )
        yield this.items[key];
    }
  }
}

Si è dotata la classe di un metodo add() per aggiungere elementi all’array, sfruttando il metodo push sull’array interno.

La parte più interessante è quella della funzione generatore, in cui sfrutto questo meccanismo, infatti come enunciato in precedenza: se associamo il generatore di iteratori alla proprietà che corrisponde al Symbol.iterator, il for of sarà in grado di usare automaticamente il meccanismo.

Creiamo quindi una funzione generatrice ( col simbolo *) dandole come identificatore il Symbol.iterator. In questo modo il for of dopo averla rilevata, chiederà un iterator alla classe e utilizzerà il metodo next per svolgere il suo compito

* [Symbol.iterator]() 
{ 
  for (let key in this.items) 
   { 
     if(this.items[key].length>4 ) 
     yield this.items[key]; 
    } 
 }

Nell’esempio si è utilizzato il for in

for (let key in this.items)

per ogni key che trovo all’interno (in) dell’array, effettuo un’operazione.

Si è messo poi un’operazione e un controllo qualunque, semplicemente per dimostrazione che è possibile effettuare qualunque cosa, che sarà restituita con yield al for of.

Nel nostro esempio, l’iterato eri restituisce le stringhe con lunghezza maggiore di 4 (metodo lenght)

 if(this.items[key].length>4 ) 
yield this.items[key];

Tradotto: se la stringa (key) dell’array this.items = [] è minore di 4 ( .length>4 ) allora verrà restituita da yield, altrimenti sarà esclusa.

Vediamo l’output, creiamo l’oggetto di classe miaCollezione() e aggiungiamo delle stringhe

let o = new miaCollezione();
o.add("rossi");
o.add("Bob");
o.add("Gialli");

notate che Bob è solo 3 caratteri quindi sarò saltato.

Creiamo il ciclo for sull’oggetto

for (x of o) { writeln(x); }

Secondo la specifica impostata nella nostra classe, saranno stampati solo rossi e Gialli.

Siamo limitati ad un solo iterator per classe? La risposta è no, quello utilizzato è quello standard che sfrutta il Symbol.iterator associato al for of, nulla vieta di creare un’altra funzione generatrice, aggiungiamone una di esempio che restituisca banalmente solo gli elementi dell’array

* generatrice()
{
  for (let key in this.items)
  {
      yield this.items[key];
  }      
}

con questa funzione non possiamo sfruttare il meccanismo del for of, ma procederemo in questo modo: andiamo a creare un oggetto iterator che lancia la funzione generatrice dell’oggetto o creato prima sulla classe miaCollezione.

let iterator = o.generatrice();

a questo punto creo un ciclo while con la condizione che la componente done si true

while (! (elemento=iterator.next()).done )
  writeln( elemento.value );

Quando la condizione viene soddisfatta, ovvero si è stampato l’ultimo elemento dell’array, la condizione diventa TRUE e il ciclo si interrompe.

Abbiamo ottenuto lo stesso risultato del for of su una seconda funzione generatrice.