|
Concludiamo questo intermezzo prima di affrontare le funzioni parlando brevemente degli iteratori. Come avrete capito anche nei capitoli precedenti, si tratta di strumenti che ci permettono di visitare gli elementi di una collezione (anche se il discorso delle collezioni è ampio e sarà trattato a parte) e di lavorare su di essi. Il materiale è ampio e qui darò solo dei concetti di base utili per lavorare e capire il discorso nel suo complesso. Va detto che si tratta, per via della loro implementazioni, di costrutti molto duttili ed efficienti. Ad esempio, gli iteratori in Rust sono progettati per essere zero-cost abstractions. Ciò significa che usano il monomorfismo e il compilatore LLVM (tale framework, lo ricordiamo, è il motore sottostante a questo linguaggio) per generare codice altamente efficiente, evitando overhead di runtime o allocazioni di memoria extra. Inoltre gli iteratori implementano il trait drop evitando effetti collaterali che potrebbero creare problemi prestazionali. Si tratta di elementi che implementano il trait Iterator e che adottano un approccio di tipo lazy (pigro). Quest'ultimo aspetto implica che gli elementi vengono effettivamente diciamo "consumati" solo quando ciò è effettivamente richiesto dal codice, ovvero solo quando è effettivamente necessario. Il tutto va a vantaggio delle prestazioni e della gestione delle risorse, in generale. Creare un iteratore è abbastanza semplice: dichiariamo prima una sequenza e poi lo definiamo a partire da questa: let v1 = vec![1,2,3,4]; let iter = v1.iter(); ecco fatto, con due semplici istruzioni abbiamo definito il nostro iter che ci permetterà di lavorare sui singoli elementi del vettore. Vediamo un semplice esempio completo:
iter non è ovviamente l'unico iteratore che possiamo utilizzare (lo vedremo comunque all'opera anche in un esempio nel capitolo delle funzioni). Molto interessante è into_iter che consuma la sorgente dei dati svuotandola. Nell'esempio seguente usiamo uno dei metodi più noti e comuni usati in Rust quando si tratta di sequenze ovvero next() fondamentale nel trait Iterator. Next(), come dice il nome stesso, viene usato per recuperare il prossimo elemento in una sequenza. Restituisce Some(elemento) se vi è ancora un elemento incontrato durante il suo avanzamento e None se non vi è più nulla. Ecco un breve esempio di utilizzo pratico:
Di frequente incontrerete anche iter_mut il quale fornisce riferimenti mutabili ad un collezione. Su tali riferimenti è possibile lavorare, come nel seguente semplice e classico esempio:
dove ad ogni elemento su cui agisce l'iteratore viene aggiunto il numero 5. Non preoccupatevi per ora di quel * che capiremo bene nel capitolo dedicato ai puntatori. In questo caso è necessario usare quell'operatore, in questo caso è un operatore di dereferenziazione, in quanto, come detto, iter_mut non restituisce un intero ma un riferimento. Molto simile al calssico for ma usato per un approccio funzionale è for_each. Lo mostriamo in azione nel seguente esempio, dove trovate una anticipazione delle chiusure, oggetto di una dei capitoli più avanti:
Abbiamo già incontrato nei capitoli scorsi un utilissimo metodo di iterazione ovvero nth(n). Esso viene utilizzato per accedere all’elemento in una posizione specifica all’interno di un iteratore. Quando chiamiamo nth(n) su un iteratore, ci restituisce un’opzione (Option) che contiene l’elemento alla posizione n (dove la numerazione inizia da zero). Se l’iteratore ha meno di n + 1 elementi, nth(n) restituirà None. Ecco l'esempio:
I commenti spiegano bene quello che accade, di nostro interesse ovviamente sono risultano in particolare la riga 4 e la 10 dove entra in azione nth(n). La cosa non finisce qui in quanto le manipolazioni possibili sono davvero tante. Mostro di seguito un esempio completo che potrete rivedere una volta che avremo affrontato il capitolo dedicato ai puntatori:
Lo scopo dell'esempio è mostrare un uso completo dei valori estratti grazie a nth(). Per tutte queste iterazioni è disponibile anche possibilità di eseguire uno step incrementale diverso da 1. L'esempio seguente incrementa di due unità ad ogni passo, in questi casi si usa step_by(num), vedi l'esempio seguente (poco originale, lo so... lo trovate più o meno dappertutto):
Diamo ora una sguardo a map che in pratica può essere utilizzato in congiunzione con iter per trasformare ogni elemento di un iteratore in un altro valore, producendo un nuovo iteratore che contiene i risultati delle trasformazioni. Internamente la definizione di map non è proprio immediata e bisogna conoscere il concetto di chiusura che è l'argomento richiesto, ma dal punto di vista pratico in molti casi il suo uso più facile a farsi che a dirsi, come dimostra il seguente esempio:
con questo output:
come si vede map usa come parametro, quel |x| che trovate subito dopo la chiamata a map, i valori che vengono ricavati dall'iterazione. Si noti la presenza di collect alla riga 4, metodo necessario al fine di ricreare una sequenza. Necessario perchè, senza di esso, map praticamente non genera nulla. Forse ancora meglio, alla riga 4 |x| può essere sostituito da |&x| . Per chi fosse curioso di vedere come funzionano le cose under the hood, internamente collect è definita come segue: fn collect<B: FromIterator<Self::Item>>(self) -> B where Self: Sized, { FromIterator::from_iter(self) } in breve: collect restituisce un tipo B che deve implementare il trait FromIterator, <Self::Item> si riferisce al fatto che verrà preso in carico il tipo dell'iteratore mentre la clausola where impone che la dimensione del tipo deve essere nota a tempo di compilazione. L'implementazione della funzione chiama in causa from_iter che si occupa di creare la nuova collezione. I metodi che trovate nel trait Iterator sono davvero tanti. Questo breve excursus non può coprirli tutti. L'ultimo che mi pare molto utile per lavorare in manira proficua è zip, che abbiamo già incontrato. Questo in pratica combina due sequenze creando una sequenza di ennuple dove il primo elemento di ciascuna ennupla viene dalla prima sequenza. Vediamo anche qui un esempio:
che ci dà:
Molto comodo è enumerate che trasforma un iteratore in un iteratore che restituisce ogni elemento con il suo indice. In pratica avremo una ennupla contenente una coppia (indice, elemento). Ecco l'esempio:
ed ecco qua le nostre ennuple:
Concludo evidenziando altri iteratori meno usati ma utili in qualche occasione: std::iter::repeat(): Produce un iteratore che ripete infinitamente un valore specifico. std::iter::once(): Restituisce un iteratore che produce un singolo elemento una sola volta. std::iter::empty(): Crea un iteratore che non produce alcun elemento. Diamo un esempio:
Fa la sua comparsa anche take che è utilizzato dal linguaggio per limitare il numero di elementi preso in carico da un iteratore. Un altro frammento di codice che lo vede in azione è il seguente: let numbers = vec![1, 2, 3, 4, 5]; // Crea un iteratore che restituirà solo i primi 3 elementi. let limited_numbers: Vec<_> = numbers.iter().take(3).collect(); Piuttosto utile può essere anche peek che permette di esaminare l'elemento successivo senza consumarlo. Questo può essere utile in algoritmi dove è necessario guardare avanti nel flusso di dati. Ottimo in congiunzione con into_iter
Può essere utile prima di chiudere riassumere quegli adattatori che permettono di filtrare i risultati degli iteratori, che forse ho presentato in modo un po' caotico. In particolare parliamo di alcuni tra i più usati e che rivediamo in un esempio che li include tutti:
map: Trasforma ogni elemento. filter: Filtra gli elementi in base a una condizione. take: Limita il numero di elementi. skip: Salta un certo numero di elementi. In considerazione di ciò l'output sarà:
Per quanto sia argomento avanzato, ma per dimostrare ancora di più la grande duttilità degli iteratori, è interessante notare che usando il crate rayon, possiamo rendere gli iteratori paralleli per sfruttare più core della CPU. Questa libreria consente di iterare sui dati in parallelo in modo sicuro, mantenendo lo stesso stile funzionale degli iteratori tradizionali. Come detto è argomento avanzato e lo affronteremo più avanti. Consiglio di dare uno sguardo alla sezione ufficiale dedicata agli iteratori in quanto c'è veramente molto materiale che può risultare utile. |