2 marzo 2021

Iteratori

Gli oggetti iterabili sono una generalizzazione degli array. Questo concetto consente a qualsiasi oggetto di essere utilizzato in un ciclo for..of.

Ovviamente, gli array sono oggetti iterabili. Ma ci sono molti altri oggetti integrati, che sono altresì iterabili. Ad esempio, le stringhe.

Se un oggetto rappresenta una collezione (lista, insieme) di qualcosa, allora for..of è un ottimo modo per eseguire un ciclo, quindi ora vedremo come farlo funzionare correttamente.

Symbol.iterator

Possiamo spiegare meglio il funzionamento degli oggetti iterabili costruendone uno nostro.

Ad esempio, abbiamo un oggetto, che non è un array, ma sembra essere adatto ad un for..of.

Come un oggetto range che rappresenta un intervallo numerico:

let range = {
  from: 1,
  to: 5
};

// Vorremmo che il for..of funzioni:
// for(let num of range) ... num=1,2,3,4,5

Per rendere iterabile l’oggetto range (e poter quindi utilizzare correttamente for..of) abbiamo bisogno di aggiungervi un metodo chiamato Symbol.iterator (uno speciale simbolo integrato).

  1. Quando for..of inizia, prova a chiamare questo metodo (o ritorna un errore se non lo trova). Il metodo deve ritornare un iteratore – un oggetto con il metodo next.
  2. La possibilità di avanzare di for..of funziona solamente con l’oggetto ritornato.
  3. Quando for..of vuole il prossimo valore, chiama next() su quell’oggetto.
  4. Il risultato di next() deve avere la forma {done: Boolean, value: any}, dove done=true significa che l’iterazione è completa, altrimenti value deve contenere il valore successivo.

Qui l’implementazione completa per range:

let range = {
  from: 1,
  to: 5
};

// 1. la chiamata a for..of inizialmente chiama questo
range[Symbol.iterator] = function() {

  // ...ritorna l'oggetto iteratore:
  // 2. Da qui in poi, for..of lavora solamente con questo iteratore, richiedendogli il prossimo valore
  return {
    current: this.from,
    last: this.to,

    // 3. next() viene invocato ad ogni iterazione del ciclo for..of
    next() {
      // 4. dovrebbe ritornare il valore sotto forma di oggetto {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// ora funziona !
for (let num of range) {
  alert(num); // 1, poi 2, 3, 4, 5
}

Da notare la caratteristica fondamentale degli oggetti iterabili: un importante separazione di concetti:

  • Il range stesso non possiede un metodo next().
  • Invece, un altro oggetto, detto “iteratore”, viene creato dalla chiamata range[Symbol.iterator](), e gestisce l’intera iterazione.

Quindi, l’oggetto iteratore è separato da quello su cui itera.

Tecnicamente, per rendere il codice più semplice, potremmo unirli e utilizzare range stesso.

Come nel seguente codice:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  alert(num); // 1, poi 2, 3, 4, 5
}

Ora range[Symbol.iterator]() ritorna l’oggetto range stesso: questo ha il metodo next() necessario e memorizza il progresso dell’iterazione in this.current. Più corto? Sì. E molte volte può andare bene.

Il lato negativo è che ora è impossibile avere due cicli for..of che iterano sull’oggetto contemporaneamente: infatti condividerebbero lo stesso stato di iterazione, poiché c’è solo un oggetto iteratore – l’oggetto stesso. In ogni caso due cicli for..of simultanei sono molto rari, persino in alcuni scenari asincroni.

Iteratori infiniti

Sono possibili anche iteratori infiniti. Ad esempio, l’oggetto range diventa infinito quando range.to = Infinity. Oppure possiamo creare un oggetto iterabile che generi un’infinita sequenza di numeri pseudo-casuali.

Non c’è alcun limite per next: può ritornare più e più valori.

Ovviamente, il ciclo for..of diventerebbe infinito. Possiamo comunque fermarlo con break.

Le stringhe sono iterabili

Gli array e le stringhe sono gli oggetti su cui si utilizzano di più gli iteratori.

Per una stringa, for..of itera sui caratteri:

for (let char of "test") {
  // attivato 4 volte: una volta per ogni carattere
  alert( char ); // t, poi e, poi s, poi t
}