Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Uno Sguardo Più Da Vicino ai Trait per Async

Nel corso del capitolo, abbiamo utilizzato i trait Future, Stream e StreamExt in vari modi. Finora, però, abbiamo evitato di addentrarci troppo nei dettagli di come funzionano o di come interagiscono, il che va bene per la maggior parte delle volte che li userai nel tuo lavoro quotidiano con Rust. A volte, però, ti capiterà di incontrare situazioni in cui avrai bisogno di comprendere queste cose più in dettaglio. In questa sezione, ci addentreremo il giusto in questi dettagli per aiutarti in quegli scenari, lasciando comunque il vero e proprio approfondimento completo alla documentazione specifica di quello che ti interessa.

Il Trait Future

Iniziamo a dare un’occhiata più da vicino a come funziona il trait Future. Ecco come Rust lo definisce:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Quella definizione di trait include alcuni nuovi type e anche una sintassi che non abbiamo visto prima d’ora, quindi esaminiamola un pezzo per volta.

Per prima cosa, il type associato Output di Future dice in cosa si risolve la future. Questo è analogo al type associato Item per il trait Iterator. In secondo luogo, Future ha il metodo poll, che prende un reference speciale Pin per il suo parametro self e un reference mutabile a un type Context, e restituisce un Poll<Self::Output>. Parleremo più avanti di Pin e Context. Per ora, concentriamoci su cosa restituisce il metodo, il type Poll:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

Questo type Poll è simile a un Option. Ha una variante che ha un valore, Ready(T), e una che non ce l’ha, Pending (occupata). Tuttavia, Poll significa qualcosa di molto diverso da Option! La variante Pending indica che la future ha ancora lavoro da fare, quindi il chiamante dovrà controllare di nuovo più tardi. La variante Ready indica che la Future ha finito il suo lavoro e il valore T è disponibile.

Nota: È raro dover chiamare poll direttamente, ma se devi, tieni a mente che con la maggior parte delle future, il chiamante non dovrebbe chiamare poll di nuovo dopo che la future ha restituito Ready. Molte future andranno in panic se interrogate di nuovo dopo essere diventate pronte. Le future che possono essere interrogate di nuovo lo diranno esplicitamente nella loro documentazione. Questo è simile a come si comporta Iterator::next.

Quando vedi codice che usa await, Rust lo compila dietro le quinte in codice che chiama poll. Ritornando per un attimo al Listato 17-4, dove abbiamo stampato il titolo della pagina per un singolo URL, Rust lo compila in qualcosa di simile (anche se non esattamente) a questo:

match titolo_pagina(url).poll() {
    Ready(valore) => match titolo_pagina {
        Some(titolo) => println!("Il titolo per {url} era {titolo}"),
        None => println!("{url} non aveva titolo"),
    }
    Pending => {
        // Ma cosa mettiamo qui?
    }
}

Cosa dovremmo fare quando la future è ancora Pending? Abbiamo bisogno di un modo per riprovare, e riprovare, e riprovare, fino a quando la future è finalmente pronta. In altre parole, abbiamo bisogno di un ciclo:

let mut titolo_pagina_fut = titolo_pagina(url);
loop {
    match titolo_pagina_fut.poll() {
        Ready(valore) => match titolo_pagina {
            Some(titolo) => println!("Il titolo per {url} era {titolo}"),
            None => println!("{url} non aveva titolo"),
        }
        Pending => {
            // continua
        }
    }
}

Se Rust lo compilasse esattamente in quel codice, però, ogni await sarebbe bloccante, esattamente l’opposto di ciò che volevamo! Invece, Rust si assicura che il ciclo possa cedere il controllo a qualcosa che può mettere in pausa il lavoro su questa future per lavorare su altre future e poi controllare di nuovo questo più tardi. Come abbiamo visto, quel qualcosa è un runtime async, e questo lavoro di pianificazione e coordinamento è uno dei suoi compiti principali.

Nella sezione “Inviare Dati Tra Due Task Usando il Passaggio di Messaggi”, abbiamo descritto l’attesa su rx.recv. La chiamata recv restituisce una future, e attendere la future la richiama. Abbiamo notato che un runtime metterà in pausa la future fino a quando è pronta o con Some(messaggio) o con None quando il canale si chiude. Ora che comprendi meglio il trait Future, e specificamente Future::poll, possiamo vedere come funziona. Il runtime sa che la future non è pronta quando restituisce Poll::Pending. Al contrario, il runtime sa che la future è pronta e la avanza quando poll restituisce Poll::Ready(Some(messaggio)) o Poll::Ready(None).

I dettagli esatti di come un runtime faccia ciò vanno oltre lo scopo di questo libro, ma la chiave è vedere i meccanismi di base delle future: un runtime interroga ogni future di cui è responsabile, rimettendo la future a dormire quando non è ancora pronta.

Il Type Pin e il Trait Unpin

Nel Listato 17-13 abbiamo usato la macro trpl::join! per unire e aspettare tre future. È tuttavia comune avere una collezione come un vettore che contiene un certo numero di future non conoscibile se non durante l’esecuzione del programma. Apportiamo delle modifiche al Listato 17-13 per mettere le tre future in un vettore e chiamare la funzione trpl::join_all al posto della macro, cosa che per ora non si compilerà.

File: src/main.rs
extern crate trpl; // necessario per test mdbook

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let valori = vec![
                String::from("ciao"),
                String::from("dalla"),
                String::from("future"),
                String::from("!!!"),
            ];

            for val in valori {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(valore) = rx.recv().await {
                println!("ricevuto '{valore}'");
            }
        };

        let tx_fut = async move {
            // --taglio--
            let valori = vec![
                String::from("altri"),
                String::from("messaggi"),
                String::from("per"),
                String::from("te"),
            ];

            for val in valori {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let future: Vec<Box<dyn Future<Output = ()>>> =
            vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

        trpl::join_all(future).await;
    });
}
Listato 17-23: Attesa di future in una collezione

Incapsuliamo ciascuna future in una Box rendendole oggetti trait, proprio come abbiamo fatto nella sezione “Restituire Errori dalla Funzione esegui del Capitolo 12. (Parleremo degli oggetti trait in dettaglio nel Capitolo 18.) Usare oggetti trait ci permette di trattare ciascuna delle future anonime prodotte da questi type come fossero il medesimo type, perché tutti implementano il trait Future.

Questo potrebbe essere sorprendente. Dopotutto, nessuno dei blocchi async restituisce nulla, quindi ciascuno produce un Future<Output = ()>. Ricorda che Future è un trait, e che il compilatore crea una enum univoca per ogni blocco async anche se hanno type di output identici. Non puoi mettere due struct scritte a mano diverse in un Vec, e la stessa regola si applica alle enum diverse generate dal compilatore.

Passiamo quindi la collezione di future alla funzione trpl::join_all e aspettiamo il risultato. Tuttavia, questa modifica non viene compilata; ecco la parte rilevante dei messaggi di errore:

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:51:32
   |
51 |         trpl::join_all(future).await;
   |                                ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope

La nota in questo messaggio di errore ci dice che dovremmo usare la macro pin! per fissare i valori, il che significa incapsularli nel type Pin che garantisce il fatto che questi valori non vengano spostati nella memoria. Il messaggio di errore dice che il pinning è richiesto perché dyn Future<Output = ()> deve implementare il trait Unpin, cosa che al momento non fa.

La funzione trpl::join_all restituisce una struct chiamata JoinAll. Quella struct è generica su un type F, che è vincolato a implementare il trait Future. Attendere direttamente una future con await blocca implicitamente la future. Ecco perché non abbiamo bisogno di usare pin! ovunque vogliamo attendere le future.

Tuttavia, qui non stiamo attendendo direttamente una future. Invece, costruiamo un nuova future, JoinAll, passando una collezione di future alla funzione join_all. La firma per join_all richiede che i type degli elementi nella collezione implementino tutti il trait Future, e Box<T> implementa Future solo se il T che incapsula è una future che implementa il trait Unpin.

Sono un sacco di informazioni da assorbire! Per capire davvero, approfondiamo un po’ di più come funziona effettivamente il trait Future, in particolare riguardo al pinning. Guarda di nuovo la definizione del trait Future:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // Metodo richiesto
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Il parametro cx e il suo type Context sono la chiave per capire come un runtime sa effettivamente quando controllare una data future pur rimanendo lazy. Ancora una volta, i dettagli di come ciò funzioni vanno oltre lo scopo di questo capitolo, e generalmente devi pensare a questo solo quando scrivi un’implementazione personalizzata di Future. Ci concentreremo invece sul type per self, poiché è la prima volta che vediamo un metodo in cui self ha un’annotazione di type. Un’annotazione di type per self funziona come le annotazioni di type per altri parametri di funzione ma con due differenze chiave:

  • Indica a Rust di quale type deve essere self affinché il metodo possa essere chiamato.
  • Non può essere semplicemente qualsiasi type. È limitato al type su cui il metodo è implementato, a un reference o a un puntatore intelligente a quel type, o a un Pin che incapsula un reference a quel type.

Esploreremo maggiormente questa sintassi nel Capitolo 18. Per ora, è sufficiente sapere che se vogliamo interrogare una future per controllare se è Pending o Ready(Output), abbiamo bisogno di un reference mutabile al type incapsulato in Pin.

Pin è un wrapper per type simili a puntatori come &, &mut, Box e Rc. (Tecnicamente, Pin funziona con type che implementano i trait Deref o DerefMut, ma questo è effettivamente equivalente a lavorare solo con reference e puntatori intelligenti.) Pin di per sé non è un puntatore e non ha alcun comportamento proprio, come invece Rc e Arc fanno con il conteggio dei reference; è puramente uno strumento che il compilatore può utilizzare per imporre vincoli sull’uso dei puntatori.

Ricordando che await è implementato in termini di chiamate a poll, iniziamo a capire il messaggio di errore che abbiamo visto in precedenza, ma quello era in termini di Unpin, non di Pin. Quindi, come si relazionano esattamente Pin e Unpin, e perché il Future ha bisogno che self sia in un type Pin per chiamare poll?

Come menzionato in precedenza nel capitolo, una serie di punti di attesa in una future viene compilata in una macchina a stati, e il compilatore si assicura che quella macchina a stati segua tutte le normali regole di sicurezza di Rust, inclusi il prestito e la ownership. Per far funzionare tutto ciò, Rust guarda quali dati sono necessari tra un punto di attesa e l’altro, o tra il punto di attesa e la fine del blocco async. Crea quindi una variante corrispondente nella macchina a stati compilata. Ogni variante ottiene l’accesso di cui ha bisogno ai dati che verranno utilizzati in quella sezione del codice sorgente, o prendendo possesso di quei dati o ottenendo un reference mutabile o immutabile ad essi.

Finora, tutto bene: se commettiamo errori riguardo alla ownership o ai reference in un dato blocco async, il borrow checker ce lo dirà. Quando vogliamo spostare la future che corrisponde a quel blocco, come spostarla in un Vec da passare a join_all, le cose diventano più complicate.

Quando spostiamo una future, sia mettendola in una struttura dati da utilizzare come iteratore con join_all o restituendola da una funzione, significa effettivamente spostare la macchina a stati che Rust crea per noi. E a differenza della maggior parte degli altri type in Rust, le future che Rust crea per i blocchi async può capitare abbiano riferimenti a se stesse nei campi di una data variante, come mostrato nell’illustrazione semplificata nella Figura 17-4.

Una tabella a colonna singola e
tre righe che rappresenta una `future`, fut1, che ha valori di dati 0 e 1 nelle
prime due righe e una freccia che punta dalla terza riga di nuovo alla seconda
riga, rappresentando un riferimento interno all’interno della `future`.

Figura 17-4: Un tipo di dato auto-referenziale

Per impostazione predefinita, però, qualsiasi oggetto che ha un riferimento a se stesso è insicuro da spostare, perché i riferimenti puntano sempre all’indirizzo di memoria effettivo di ciò a cui si riferiscono (vedi Figura 17-5). Se sposti la struttura dati stessa, quei riferimenti interni rimarranno puntati alla vecchia posizione. Tuttavia, quella posizione di memoria è ora non valida. Per un verso, il suo valore non verrà aggiornato quando apporti modifiche alla struttura dati. Per un altro e più importante motivo, il computer è ora libero di riutilizzare quella memoria per altri scopi! Potresti finire per leggere dati completamente non correlati in seguito.

Due tabelle, che raffigurano
due `future`, fut1 e fut2, ciascuna delle quali ha una colonna e tre righe,
rappresentando il risultato di aver spostato una `future` da fut1 a fut2. La
prima, fut1, è grigia, con un punto interrogativo in ciascun indice,
rappresentando una memoria sconosciuta. La seconda, fut2, ha 0 e 1 nella prima e
nella seconda riga e una freccia che punta dalla sua terza riga di nuovo alla
seconda riga di fut1, rappresentando un puntatore che fa riferimento alla
vecchia posizione in memoria della `future` prima che fosse spostata.

Figura 17-5: Il risultato non sicuro di spostare un tipo di dato auto-referenziale

Teoricamente, il compilatore Rust potrebbe cercare di aggiornare ogni riferimento a un oggetto ogni volta che viene spostato, ma ciò potrebbe aggiungere un notevole sovraccarico di operazioni impattando le prestazioni, specialmente se un’intera rete di riferimenti deve essere aggiornata. Se potessimo invece assicurarci che la struttura dati in questione non si muova in memoria, non dovremmo aggiornare alcun riferimento. Questo è esattamente a ciò che serve il borrow checker di Rust: nel codice sicuro, impedisce di spostare qualsiasi elemento con un riferimento attivo.

Pin si basa su questo per darci la garanzia esatta di cui abbiamo bisogno. Quando fissiamo un valore incapsulando un puntatore a quel valore in Pin, non può più muoversi. Quindi, se hai Pin<Box<QualcheType>>, in realtà fissi il valore QualcheType, non il puntatore Box. La Figura 17-6 illustra questo processo.

Tre scatole disposte
affiancate. La prima è etichettata “Pin”, la seconda “b1”, e la terza “pinned”.
All’interno di “pinned” c’è una tabella etichettata “fut”, con una singola
colonna; rappresenta una `future` con celle per ciascuna parte della struttura
dati. La sua prima cella ha il valore “0”, la sua seconda cella ha una freccia
che esce da essa e punta alla quarta e ultima cella, che ha il valore “1”, e la
terza cella ha linee tratteggiate e un’ellissi per indicare che potrebbero
esserci altre parti nella struttura dati. Insieme, la tabella “fut” rappresenta
una `future` che è auto-referenziale. Una freccia esce dalla scatola etichettata
“Pin”, passa attraverso la scatola etichettata “b1” e termina all’interno della
scatola “pinned” nella tabella “fut”.

Figura 17-6: Pinning di una Box che punta a un type future auto-referenziale

In effetti, il puntatore Box può ancora muoversi liberamente. Ricorda: ci interessa assicurarci che i dati a cui si fa riferimento rimangano al loro posto. Se un puntatore si muove, ma i dati a cui punta sono nello stesso posto, come nella Figura 17-7, non c’è alcun problema potenziale. (Come esercizio indipendente, dai un’occhiata alla documentazione per i type così come a quella del modulo std::pin e prova a capire come faresti questo con un Pin che incapsula una Box.) La chiave è che il type auto-referenziale stesso non può muoversi, perché è ancora fissato.

Quattro scatole disposte in tre
colonne approssimative, identiche al diagramma precedente con una modifica alla
seconda colonna. Ora ci sono due scatole nella seconda colonna, etichettate “b1”
e “b2”, “b1” è grigia, e la freccia da “Pin” passa attraverso “b2” invece di
“b1”, indicando che il puntatore si è spostato da “b1” a “b2”, ma i dati in
“pinned” non si sono mossi.

Figura 17-7: Spostare una Box che punta a un type future auto-referenziale

Tuttavia, la maggior parte dei type è perfettamente sicura da spostare, anche se sono incapsulati da Pin. Dobbiamo pensare al pinning solo quando gli elementi hanno reference interni. I valori primitivi come numeri e booleani sono sicuri perché ovviamente non hanno reference interni. Né la maggior parte dei type con cui normalmente lavori in Rust. Puoi spostare un Vec, ad esempio, senza preoccuparti. Dato ciò che abbiamo visto finora, se hai un Pin<Vec<String>>, dovresti fare tutto tramite le API sicure ma restrittive fornite da Pin, anche se un Vec<String> è sempre sicuro da spostare se non ci sono altri riferimenti ad esso. Abbiamo bisogno di un modo per dire al compilatore che va bene spostare gli elementi in casi come questo, ed è qui che entra in gioco Unpin.

Unpin è un trait marcatore, simile ai trait Send e Sync che abbiamo visto nel Capitolo 16, e quindi non ha funzionalità propria. I trait marcatori esistono solo per dire al compilatore che è sicuro utilizzare il type che implementa un dato trait in un contesto particolare. Unpin informa il compilatore che un dato type non ha bisogno di verificare alcuna garanzia sul fatto che il valore in questione possa essere spostato in sicurezza.

Proprio come per Send e Sync, il compilatore implementa automaticamente Unpin per tutti i type per i quali può dimostrare che è sicuro. Un caso speciale, di nuovo simile a Send e Sync, è dove Unpin non è implementato per un type. La notazione per questo è impl !Unpin for QualcheType, dove QualcheType è il nome di un type che deve mantenere quelle garanzie per essere sicuro ogni volta che un puntatore a quel type viene utilizzato in un Pin.

In altre parole, ci sono due cose da tenere a mente riguardo alla relazione tra Pin e Unpin. Prima di tutto, Unpin è il caso “normale”, e !Unpin è il caso speciale. In secondo luogo, se un type implementa Unpin o !Unpin importa solo quando stai usando un puntatore fissato a quel type come Pin<&mut QualcheType>.

Per andare nel concreto, pensa a una String: ha una lunghezza e i caratteri Unicode che la compongono. Possiamo incapsulare una String in Pin, come vedi nella Figura 17-8. Tuttavia, String implementa automaticamente Unpin, così come la maggior parte degli altri type in Rust.

Un contenitore etichettato
“Pin” sulla sinistra con una feccia che parte da esso e punta ad un contenitore
etichettato “String” sulla destra. Il contenitore “String” contiene il dato
5usize, che rappresenta la lunghezza della stringa, e le lettere “h”, “e”, “l”,
“l” e “o” rappresentanti i caratteri della stringa “hello” memorizzata in questa
istanza di String. Un rettangolo tratteggiato circonda il contenitore “String” e
la sua etichetta, ma non il contenitore “Pin”.

Figura 17-8: Pinning di una String; la linea tratteggiata indica che la String implementa il trait Unpin e quindi non è fissata

Di conseguenza, possiamo fare cose che sarebbero illegali se String implementasse !Unpin, come sostituire una stringa con un’altra nella stessa posizione in memoria, come nella Figura 17-9. Questo non viola il contratto di Pin, perché String non ha riferimenti interni che la rendano insicura da spostare. È proprio per questo che implementa Unpin piuttosto che !Unpin.

La medesima stringa “hello”
dell'esempio precedente, ora etichettata “s1” e sbiadita. Il contenitore “Pin”
dell'esempio precedente ora punta ad una differente istanza di String,
etichettata “s2”, valida, con lunghezza 7usize, e contenente i caratteri della
stringa “goodbye”. s2 è circondata da un rettangolo tratteggiato perché,
anch'essa, implementa il trait Unpin.

Figura 17-9: Sostituzione della String con un’altra String completamente diversa in memoria

Ora sappiamo abbastanza per comprendere gli errori segnalati per quella chiamata a join_all dal Listato 17-23. Inizialmente abbiamo cercato di spostare le future prodotte dai blocchi async in un Vec<Box<dyn Future<Output = ()>>>, ma come abbiamo visto, quelle future possono avere riferimenti interni, quindi non implementano automaticamente Unpin. Una volta fissate, poi possiamo passare il risultante type Pin nel Vec, certi che i dati sottostanti nelle future non verranno spostati. Il Listato 17-24 mostra come sistemare il codice chiamando la macro pin! dove ognuna delle tre future è definita e sistemare il type dell’oggetto trait.

extern crate trpl; // necessario per test mdbook

use std::pin::{Pin, pin};

// --taglio--

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = pin!(async move {
            // --taglio--
            let valori = vec![
                String::from("ciao"),
                String::from("dalla"),
                String::from("future"),
                String::from("!!!"),
            ];

            for val in valori {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let rx_fut = pin!(async {
            // --taglio--
            while let Some(valore) = rx.recv().await {
                println!("ricevuto '{valore}'");
            }
        });

        let tx_fut = pin!(async move {
            // --taglio--
            let valori = vec![
                String::from("altri"),
                String::from("messaggi"),
                String::from("per"),
                String::from("te"),
            ];

            for val in valori {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let future: Vec<Pin<&mut dyn Future<Output = ()>>> =
            vec![tx1_fut, rx_fut, tx_fut];

        trpl::join_all(future).await;
    });
}
Listato 17-24: Usare pin! per consentire alle future di essere spostate nel vettore

Questo esempio ora si compila ed esegue, e possiamo aggiungere o togliere future dal vettore durante l’esecuzione aspettandole tutte.

Pin e Unpin sono principalmente importanti per costruire librerie di basso livello, o quando stai costruendo un vero e proprio runtime, piuttosto che per il codice Rust quotidiano. Tuttavia, quando vedi questo trait nei messaggi di errore, ora avrai un’idea migliore di come correggere il tuo codice!

Nota: Questa combinazione di Pin e Unpin rende possibile implementare in modo sicuro un’intera classe di type complessi in Rust che altrimenti risulterebbero difficili a causa della loro auto-referenzialità. I type che richiedono Pin si presentano più comunemente nella programmazione asincrona di Rust, ma di tanto in tanto potresti vederli anche in altri contesti.

I dettagli specifici su come funzionano Pin e Unpin, e le regole che devono rispettare, sono trattati ampiamente nella documentazione API per std::pin, quindi se sei interessato a saperne di più, quello è un ottimo punto di partenza.

Se vuoi capire come funzionano le cose sotto il cofano in modo ancora più dettagliato, consulta i capitoli su gestione dell’esecuzione e pinning di Asynchronous Programming in Rust.

Il Trait Stream

Ora che hai una comprensione più profonda dei trait Future e Unpin, possiamo rivolgere la nostra attenzione al trait Stream. Come hai appreso in precedenza nel capitolo, gli stream sono simili agli iteratori asincroni. A differenza di Iterator e Future, tuttavia, Stream non ha una definizione nella libreria standard al momento della scrittura, ma c’è una definizione molto comune dal crate futures utilizzata in tutto l’ecosistema.

Rivediamo le definizioni dei trait Iterator e Future prima di vedere come un trait Stream potrebbe unirne le caratteristiche. Da Iterator, abbiamo l’idea di una sequenza: il suo metodo next fornisce un Option<Self::Item>. Da Future, abbiamo l’idea di prontezza nel tempo: il suo metodo poll fornisce un Poll<Self::Output>. Per rappresentare una sequenza di elementi che diventano pronti nel tempo, definiamo un trait Stream che mette insieme queste caratteristiche:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Il trait Stream definisce un type associato chiamato Item per il type degli elementi prodotti dallo stream. Questo è simile a Iterator, dove possono esserci da zero a molti elementi, e diverso da Future, dove c’è sempre un singolo Output, anche se è il type unitario ().

Stream definisce anche un metodo per ottenere quegli elementi. Lo chiamiamo poll_next, per chiarire che interroga nello stesso modo in cui fa Future::poll e produce una sequenza di elementi nello stesso modo in cui fa Iterator::next. Il suo type di ritorno combina Poll con Option. Il type esterno è Poll, perché deve essere controllato per prontezza, proprio come una future. Il type interno è Option, perché deve segnalare se ci sono altri messaggi, proprio come fa un iteratore.

Qualcosa di molto simile a questa definizione diverrà probabilmente parte della libreria standard di Rust in futuro. Nel frattempo, fa parte dell’arsenale della maggior parte dei runtime, quindi puoi fare affidamento su di essa, e tutto ciò che copriremo successivamente dovrebbe generalmente applicarsi!

Nell’esempio che abbiamo visto nella sezione Stream: Future in Sequenza”, però, non abbiamo usato poll_next o Stream, ma invece abbiamo usato next e StreamExt. Potremmo lavorare direttamente in termini dell’API poll_next scrivendo a mano le nostre macchine a stati Stream, ovviamente, proprio come potremmo lavorare con le future direttamente tramite il loro metodo poll. Tuttavia, usare await è molto più piacevole, e il trait StreamExt fornisce il metodo next in modo da poter fare proprio questo:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // altri metodi...
}
}

Nota: La definizione effettiva che abbiamo utilizzato in precedenza nel capitolo appare leggermente diversa da questa, perché supporta versioni di Rust che non supportavano ancora l’uso di funzioni async nei trait. Di conseguenza, appare in questo modo:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

Quel type Next è una struct che implementa Future e ci consente di nominare la lifetime del reference a self con Next<'_, Self>, in modo che await possa funzionare con questo metodo.

Il trait StreamExt è anche la casa di tutti i metodi interessanti disponibili per l’uso con gli stream. StreamExt è implementato automaticamente per ogni type che implementa Stream, ma questi trait sono definiti separatamente per consentire alla comunità di aggiungere API extra senza influenzare il trait fondamentale.

Nella versione di StreamExt utilizzata nel crate trpl, il trait non solo definisce il metodo next, ma fornisce anche un’implementazione predefinita di next che gestisce correttamente i dettagli della chiamata a Stream::poll_next. Questo significa che anche quando hai bisogno di scrivere il tuo type di stream, devi solo implementare Stream, e poi chiunque utilizzi il tuo type può utilizzare StreamExt e i suoi metodi con esso automaticamente.

Questo è tutto ciò che tratteremo per i dettagli di basso livello su questi trait. Per concludere, vedremo come future (inclusi gli stream), task e thread si integrano tutti insieme!