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

Examen plus approfondi des traits pour Async

Tout au long de ce chapitre, nous avons utilisés les traits Future, Stream et StreamExt de différentes manières. Jusqu’ici, toutefois, nous avons évité d’aller trop loin dans les détails de leur fonctionnement ou de comment elles s’agencent entre elles, ce qui est acceptable la plupart du temps pour votre travail quotidien avec Rust. Parfois, cependant, vous rencontrerez des situations où vous aurez besoin de comprendre un peu plus en détail ces traits, ainsi que le type Pin et le trait Unpin. Dans cette section, nous allons approfondir juste ce qu’il faut pour vous aider dans ces cas de figure, en laissant l’analyse vraiment profonde à d’autres documents.

Le trait Future

Commençons par regarder de plus près comment fonctionne le trait Future. Voici comment Rust le définit :

#![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>;
}
}

Cette définition de trait comprend tout un tas de nouveaux types ainsi que des éléments de syntaxe que nous n’avons pas encore vus ; examinons donc cette définition point par point.

Premièrement, le type associé Output de Future indique vers quoi se résout la future. Ceci est analogue au type associé Item pour le trait Iterator. Deuxièmement, Future dispose de la méthode poll, laquelle prend une référence spéciale de type Pin comme paramètre self et une référence mutable à un type Context, et renvoie un Poll<Self::Output>. Nous reviendrons sur Pin et Context dans un moment. Pour le moment, concentrons-nous sur ce que renvoie la méthode, le type Poll.

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

Ce type Poll est similaire à une Option. Il a une variante qui a une valeur, Ready(T), et une qui n’en a pas, Pending. Toutefois, Poll veut dire quelque chose d’assez différent d’Option ! La variante Pending indique que la future a encore du travail à faire, de sorte que l’appelant devra revenir plus tard. La variante Ready indique que la Future a terminé son travail, et la valeur T est disponible.

Note : il est peu fréquent d’avoir à appeler poll directement, mais si vous en avez le besoin, gardez en tête qu’avec la plupart des futures, l’appelant ne devrait pas appeler poll de nouveau une fois que la future a renvoyé Ready. De nombreuses futures paniqueront si elles sont interrogées à nouveau après être devenues prêtes (NDT : ready). Les futures pour lesquelles une nouvelle interrogation est sans risque l’indiqueront explicitement dans leur documentation. Ceci est similaire au comportement de Iterator::next.

Quand vous voyez du code qui utilise await, sous le capot, Rust le compile en code qui appelle poll. Si vous revenez à l’encart 17-4, où nous avions affiché le titre de la page pour une unique URL une fois celle-ci résolue, Rust le compile dans quelque chose à peu près (toutefois pas exactement) comme ceci :

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(titre) => println!("Le titre de {url} était {titre}"),
        None => println!("{url} n'avait pas de titre"),
    }
    Pending => {
        // Mais qu'est-ce qui va ici ?
    }
}

Que devrions-nous faire quand la future est encore Pending ? Il nous faut un moyen d’essayer à nouveau, et encore, et encore, jusqu’à ce que la future soit finalement prête. En d’autres termes, nous avons besoin d’une boucle :

let mut page_titre_fut = page_titre(url);
loop {
    match page_titre_fut.poll() {
        Ready(valeur) => match page_titre {
            Some(titre) => println!("Le titre de {url} était {titre}"),
            None => println!("{url} n'avait pas de titre"),
        }
        Pending => {
            // continue
        }
    }
}

Cependant, si Rust compilait exactement vers ce code, chaque await serait bloquant — parfaitement l’opposé de ce que nous voulions faire ! Au lieu de cela , Rust garantit que la boucle peut céder le contrôle à quelque chose qui peut mettre le travail de cette future en pause, pour traiter d’autres futures, puis plus tard venir encore vérifier cette future. Comme nous l’avons vu, ce quelque chose est un moteur d’exécution asynchrone, et ce travail de planification et de coordination est l’une de ses principales fonctions.

Dans la section “Échange de données entre deux tâches en utilisant le passage de messages”, nous avons décrit l’attente de rx.recv. L’appel à recv renvoie une future, et attend que la future le sonde. Nous avons noté qu’un moteur d’exécution mettra la future en pause jusqu’à ce qu’il soit prêt avec soit Some(message) ou bien None quand le canal se referme. Avec notre meilleure compréhension du trait Future, et plus particulièrement de Future::poll, nous pouvons voir comme cela fonctionne. Le moteur d’exécution sait que la future n’est pas prête tant qu’elle renvoie Poll::Pending. À l’inverse, le moteur d’exécution sait que la future est prête la fait avancer lorsque poll renvoie Poll::Ready(Some(message)) ou Poll::Ready(None).

Les détails précis concernant la manière dont un moteur d’exécution accomplit cela dépassent le cadre de cet ouvrage, mais l’essentiel est de bien saisir le fonctionnement de base des futures : un moteur d’exécution interroge chaque future dont il a la charge, remettant la future en veille lorsqu’elle n’est pas encore prête.

Le type Pin et le trait Unpin

Dans l’encart 17-13, nous avions utilisé la macro trpl::join! pour attendre trois futures. Cependant, il est courant d’avoir une collection comme par exemple un vecteur qui contienne un certain nombre de futures dont le nombre ne sera connu qu’au moment de l’exécution. Modifions l’encart 17-13 pour obtenir le code de l’encart 17-23 qui dispose les trois futures dans un vecteur et appelle la fonction trpl::join_all à la place ; ceci ne se compilera pas encore.

Filename: src/main.rs
extern crate trpl; // requis par 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 vals = vec![
                String::from("salut"),
                String::from("à partir"),
                String::from("de la"),
                String::from("future"),
            ];

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

        let rx_fut = async {
            while let Some(valeur) = rx.recv().await {
                println!("reçu '{valeur}'");
            }
        };

        let tx_fut = async move {
            // -- partie masquée ici --
            let vals = vec![
                String::from("plus de"),
                String::from("messages"),
                String::from("pour"),
                String::from("vous"),
            ];

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

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

        trpl::join_all(futures).await;
    });
}
Listing 17-23: Awaiting futures in a collection

Nous disposons chaque future dans une Box pour en faire des objets traits, exactement comme nous l’avons fait dans la section “Retourner des erreurs depuis run” du chapitre 12 (nous verrons les objets traits en détail dans le chapitre 18). L’utilisation d’objets traits nous permet de traiter chacune des futures anonymes générées par ces types comme étant du même type, car ils implémentent tous le trait Future.

Ceci peut sembler surprenant. Après tout, aucun des blocs asynchrones ne renvoie rien, donc chacun produit une Future<Output = ()>. Souvenez-vous toutefois que Future est un trait, et que le compilateur crée une énumération unique pour chaque bloc asynchrone, même quand ils ont les mêmes types de retour. Exactement comme vous ne pouvez pas mettre deux structures écrites à la main dans un Vec, vous ne pouvez pas mélanger des énumérations générées par le compilateur.

Puis nous passons la collection de futures à la fonction trpl::join_all “et attendons le résultat. Cependant, ceci ne se compile pas ; voici la partie pertinente des messages d’erreur.

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).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
   = note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

La note dans ce message d’erreur nous dit que nous devrions utiliser la macro pin! pour épingler (NDT : pin) les valeurs, ce qui signifie les mettre à l’intérieur d’un type Pin qui garantit que les valeurs ne seront pas déplacées en mémoire. Le message d’erreur précise que l’épinglage (NDT : pinning) est requis car dyn Future<Output = ()> a besoin d’implémenter le trait Unpin et que, dans l’état actuel, elle ne le peut pas.

La fonction trpl::join_all renvoie une structure appelée JoinAll. Cette structure est générique sur un type F, lequel est contraint d’implémenter le trait Future. Attendre directement une future avec await épingle implicitement la future. Voilà la raison pour laquelle nous n’avons pas besoin d’utiliser pin! partout où ne voulons attendre des futures.

Toutefois, nous ne sommes pas ici en train d’attendre directement une future. À la place, nous construisons une nouvelle future, JoinAll, en passant une collection de futures à la fonction join_all. La signature de join_all requiert que les types de tous les éléments de la collection implémentent tous le trait Future, et Box<T> implémente Future seulement si le T qu’il enveloppe est une future qui implémente le trait Unpin.

Voilà qui fait beaucoup à digérer ! Pour bien comprendre, approfondissons un peu le mode de fonctionnement du trait Future, notamment en ce qui concerne l’épinglage. Revenons à la définition du trait Future trait :

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

pub trait Future {
    type Output;

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

Le paramètre cx et son type Context sont la clé expliquant comment le moteur d’exécution détermine quand il doit vérifier une future donnée tout en restant paresseux. Une fois de plus, les détails du fonctionnement dépassent le cadre de ce chapitre, et vous n’avez généralement besoin de penser à cela quand vous écrivez une implémentation personnalisée de Future. À la place, nous nous focaliserons plutôt sur le type pour self, car c’est la première fois que nous avons vu une méthode où self a une annotation de type. Une annotation de type pour self fonctionne de la même manière que les annotations de types pour d’autres paramètres de fonction, mais avec deux différences capitales :

  • elle indique à Rust de quel type self doit être pour que la méthode soit appelée ;
  • cela ne peut pas être n’importe quel type : on est restreint au type sur lequel la méthode est implémentée, une référence ou un pointeur intelligent vers ce type, ou une Pin enveloppant une référence à ce type.

Nous verrons cette syntaxe plus en détail dans le chapitre 18. Pour le moment, il suffit de savoir que si nous voulons interroger une future pour vérifier si elle est Pending ou bien Ready(Output), nous avons besoin d’une référence mutable à ce type, enveloppée par une Pin.

Pin est un conteneur pour les types de type pointeur comme &, &mut, Box et Rc (techniquement, Pin fonctionne avec des types qui implémentent les traits Deref ou DerefMut, mais cela revient en fait à travailler avec seulement des références et des pointeurs intelligents). Pin n’est pas elle-même un pointeur et n’a pas de comportement propre comme Rc et Arc en ont avec le comptage de références ; il s’agit purement d’un outil que le compilateur peut utiliser pour imposer des contraintes sur l’utilisation des pointeurs.

Le fait de rappeler que await est implémenté à l’aide d’appels à poll commence à expliquer le message vu précédemment, mais cela se rapportait à Unpin et pas à Pin. Alors, quel est au juste le lien entre Pin et Unpin, et pourquoi Future a-t-elle besoin que self soit dans un type Pin pour pouvoir appeler poll ?

Souvenez-vous, plus tôt dans ce chapitre, qu’une série de points d’attente dans une future se compile comme une machine à états, et que le compilateur s’assure que cette machine à états suit toutes les règles habituelles de Rust concernant la sécurité, y compris l’emprunt et la possession. Pour faire fonctionner tout cela, Rust détermine quelles données sont requises entre un point d’attente et soit le point d’attente suivant, soit la fin du bloc asynchrone. Chaque variante obtient l’accès dont elle a besoin aux données qui seront utilisées dans cette partie du code source, soit en prenant possession de ces données, soit en obtenant une référence mutable ou immutable à celles-ci.

Jusqu’ici, tout va bien : si nous commettons une erreur concernant la possession ou les références dans un bloc asynchrone donné, le vérificateur d’emprunt nous en informera. Quand nous voulons déplacer la future qui correspond à ce bloc — comme par exemple la déplacer dans un Vec pour la passer à join_all — les choses se compliquent.

Quand nous déplaçons une future — que ce soit en l’intégrant dans une structure de données qui sera utilisée comme un itérateur avec join_all, ou en la renvoyant depuis une fonction — ceci revient en réalité à déplacer la machine à états que Rust nous crée. Et au contraire de la plupart des types en Rust, les futures créées par Rust pour des blocs asynchrones peuvent se retrouver avec des références à elles-mêmes dans les champs d’une variante données, comme le montre l’illustration simplifiée de la figure 17-4.

A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future.
Figure 17-4: A self-referential data type

Par défaut, toutefois, il est dangereux de déplacer tout objet ayant une référence sur lui-même, parce que les références continuent de pointer vers l’emplacement mémoire réel de ce à quoi elles font références (voir figure 17-5). Si vous déplacez la structure de données elle-même, ces références internes continueront de pointer vers l’ancien emplacement. Or cet emplacement mémoire n’est désormais plus valide. D’une part, sa valeur ne sera pas mise à jour quand vous apporterez des modifications à la structure de données ; d’autre part — et c’est là le plus important —, l’ordinateur est désormais libre de réutiliser cette mémoire à d’autres fins ! Vous pourriez ensuite vous retrouver à lire des données sans aucun rapport.

Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved.
Figure 17-5: The unsafe result of moving a self-referential data type

Théoriquement, le compilateur Rust pourrait tenter de mettre à jour chaque référence vers un objet quand ce dernier se fait déplacer, mais ceci pourrait ajouter beaucoup de pertes de performances, en particulier si tout un ensemble de références doit être mises à jour. Voilà précisément ce pour quoi le vérificateur d’emprunt de Rust est fait : dans du code sûr, il vous empêche de déplacer tout élément ayant une référence active pointant vers lui.

Pin s’appuie sur ce principe pour nous apporter la garantie dont nous avons besoin. Quand nous épinglons une valeur en encapsulant un pointeur vers cette valeur dans une Pin, elle ne peut plus être déplacée. Ainsi, si vous avez Pin<Box<SomeType>>, vous épinglez en réalité la valeur SomeType valeur, pas le pointeur Box. La figure 17-6 illustre ce processus.

Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and terminates inside the “pinned” box at the “fut” table.
Figure 17-6: Pinning a `Box` that points to a self-referential future type

En fait, le pointeur Box peut continuer à se déplacer librement. Souvenez-vous : nous faisons attention à ce que ce soient les données qui sont référencées au bout du compte qui restent en place. Si un pointeur se déplace, mais que les données vers lesquelles il pointe reste au même endroit, comme dans la figure 17-7, cela ne pose aucun problème potentiel (à titre d’exercice indépendant, regardez la documentation des types ainsi que le module std::pin et tentez de déterminer comme vous vous y prendriez avec une Pin encapsulant une Box). La clé est que le type auto-référencé ne peut pas se déplacer, car il est encore épinglé.

Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved.
Figure 17-7: Moving a `Box` which points to a self-referential future type

Cependant, la plupart des types peuvent parfaitement être déplacés sans problème, même s’il se trouve qu’elles sont derrière un pointeur Pin. Nous n’avons à penser à épingler que quand des éléments ont des références internes. Les valeurs primitives comme les nombres et les booléens sont sûrs car ils n’ont bien évidemment aucune référence interne. Il en va de même pour la plupart des types avec lesquels vous travaillez normalement en Rust. Vous pouvez déplacer un Vec, par exemple, sans souci. D’après ce qu’on a vu jusqu’ici, si vous avez une Pin<Vec<String>>, vous devriez tout faire en passant par l’API, sûre mais restreinte, fournie par Pin, bien que Vec<Str> soit toujours sûr à déplacer s’il n’y a aucune autre référence vers lui. Nous avons besoin d’un moyen de dire au compilateur qu’il est sûr de déplacer des éléments dans des cas comme celui-ci — et voici le moment où Unpin intervient.

Unpin est un trait de marquage, similaire aux traits Send et Sync que nous avons vus dans le chapitre 16, il n’a donc aucune fonctionnalité qui lui soit propre. Les traits de marquage n’existent que pour indiquer au compilateur qu’il est sûr d’utiliser ce type en implémentant un trait donné dans un contexte donné. Unpin informe le compilateur qu’un type donné n’a _pas besoin de fournir de garanties quant à la question de savoir si la valeur concernée peut être déplacée en toute sécurité.

Exactement comme pour Send et Sync, le compilateur implémente Unpin automatiquement pour tous les types pour lesquels il peut prouver que c’est sécurisé. Un cas particulier, encore une fois similaire à Send et Sync, est quand Unpin n’est pas implémenté pour un type. La notation pour ceci est impl !Unpin for SomeType, où SomeType est le nom du type qui n’a pas besoin de fournir de garanties pour être sécurisé si jamais un pointeur vers ce type est utilisé dans une Pin.

Autrement dit, il y a deux choses à garder à l’esprit en ce qui concerne cette relation entre Pin et Unpin. D’abord, Unpin est le cas “normal”, et !Unpin est le cas particulier. Deuxièmement, qu’un type implémente Unpin ou bien !Unpin n’a d’importance que quand vous utilisez un pointeur épinglé vers ce type comme Pin<&mut SomeType>.

Pour rendre cela plus concret, pensez à une chaîne String : elle a une longueurs et des caractères Unicode qui la constituent. Nous pouvons encapsuler une String dans Pin, comme dans la figure 17-8. Cependant, String implémente automatiquement Unpin, comme c’est le cas la plupart des autres types en Rust.

A box labeled “Pin” on the left with an arrow going from it to a box labeled “String” on the right. The “String” box contains the data 5usize, representing the length of the string, and the letters “h”, “e”, “l”, “l”, and “o” representing the characters of the string “hello” stored in this String instance. A dotted rectangle surrounds the “String” box and its label, but not the “Pin” box.
Figure 17-8: Pinning a `String`; the dotted line indicates that the `String` implements the `Unpin` trait and thus is not pinned

Il en résulte que nous pouvons faire des choses qui auraient été illégales si String implémentait !Unpin à la place, comme par exemple le remplacement d’une chaîne par une autre à l’exact même emplacement mémoire comme dans la figure 17-9. Ceci ne viole pas le contrat de Pin, car String n’a aucune référence interne qui puisse le rendre dangereux à déplacer. Voilà précisément pourquoi il implémente Unpin plutôt que !Unpin.

The same “hello” string data from the previous example, now labeled “s1” and grayed out. The “Pin” box from the previous example now points to a different String instance, one that is labeled “s2”, is valid, has a length of 7usize, and contains the characters of the string “goodbye”. s2 is surrounded by a dotted rectangle because it, too, implements the Unpin trait.
Figure 17-9: Replacing the `String` with an entirely different `String` in memory

Nous en savons maintenant assez pour comprendre les erreurs rapportées pour cet appel à join_all de l’encart 17-23. Nous avons initialement tenté de déplacer les futures générées par des blocs asynchrones à l’intérieur d’un Vec<Box<dyn Future<Output = ()>>>, mais comme on l’a vu, ces futures pourraient avoir des références internes, elles n’implémentent donc pas automatiquement Unpin. Une fois qu’on les a épinglées, nous pouvons passer le type résultant Pin dans le Vec, confiants que les données sous-jacentes dans les futures ne seront pas déplacées. L’encart 17-24 montre comment arranger ce code en appelant la macro pin! là où chacune des trois futures est définie et en ajustant le type d’objet trait.

extern crate trpl; // requis par test mdbook

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

// -- partie masquée ici --

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 {
            // -- partie masquée ici --
            let vals = vec![
                String::from("salut"),
                String::from("à partir"),
                String::from("de la"),
                String::from("future"),
            ];

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

        let rx_fut = pin!(async {
            // -- partie masquée ici --
            while let Some(valeur) = rx.recv().await {
                println!("reçu '{valeur}'");
            }
        });

        let tx_fut = pin!(async move {
            // -- partie masquée ici --
            let vals = vec![
                String::from("plus de"),
                String::from("messages"),
                String::from("pour"),
                String::from("vous"),
            ];

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

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

        trpl::join_all(futures).await;
    });
}
Listing 17-24: Pinning the futures to enable moving them into the vector

Cet exemple se compile et s’exécute maintenant, et nous pourrions ajouter ou ôter des futures du vecteur durant l’exécution, et les regrouper toutes.

Pin et Unpin sont particulièrement importantes pour construire des bibliothèques de bas niveau, ou bien quand vous construisez un moteur d’exécution lui-même, par opposition à du code Rust du quotidien. Cependant, quand vous verrez ces traits dans les messages d’erreur, vous aurez désormais une idée bien plus précise de la manière de corriger votre code !

Note : cette combinaison de Pin et Unpin permet d’implémenter toute une catégorie de types complexes en Rust en toute sécurité, ce qui s’avèrerait autrement difficile de par leur caractère auto-référençant. Les types qui requièrent Pin apparaissent aujourd’hui le plus souvent dans du Rust asynchrone, mais de temps à autre, vous pourriez aussi les rencontrer dans d’autres contextes.

Les détails du fonctionnement de Pin et d’Unpin, ainsi que les règles qu’elles doivent respecter, sont décrits en détail dans la documentation de l’API concernant std::pin, donc si vous souhaitez en savoir plus, c’est là un excellent point de départ.

Si vous souhaitez comprendre comment les choses fonctionnent sous le capot avec encore plus de détails, voyez les chapitres 2 et 4 de Asynchronous Programming in Rust.

Le trait Stream

Maintenant que vous maîtrisez mieux les traits Future, Pin et Unpin, nous pouvons nous focaliser sur le trait Stream. Comme vous l’avez appris plus tôt dans ce chapitre, les flux sont similaires aux itérateurs asynchrones. Au contraire de Iterator et Future, cependant, Stream n’a pas de définition dans la bibliothèque standard, à l’heure où nous écrivons ces lignes, mais il y a bien une définition très courante issue de la crate futures, utilisée dans l’ensemble de l’écosystème.

Révisons les définitions des traits Iterator et Future avant de regarder comment un trait Stream pourrait les combiner ensemble. Le trait Iterator apporte la notion de séquence : sa méthode next fournit une Option<Self::Item>. Avec Future, nous avons la notion de disponibilité dans le temps : sa méthode poll fournit Poll<Self::Output>. Pour représenter une séquence d’éléments qui deviennent disponibles au fil du temps, nous définissons un trait Stream qui combine ces fonctionnalités.

#![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>>;
}
}

Le trait Stream définit un type associé appelé Item pour le type des éléments produits par le flux. Ceci est analogue à Iterator, où il peut y avoir de zéro à plusieurs éléments, et différent de Future, où il y a toujours un seul Output, même s’il s’agit du type unité ().

Stream définit également une méthode pour récupérer ces éléments. Nous l’appelons poll_next, afin de clarifier le fait qu’elle interroge de la même manière que Future::poll le fait, et qu’elle produit une séquence d’éléments de la même manière que Iterator::next le fait. Son type de retour combine Poll et Option. Le type extérieur est Poll, car il doit être vérifié pour sa disponibilité, exactement comme c’est le cas pour une future. Le type interne est Option, car il doit signaler s’il y a d’autres messages, exactement comme un itérateur le fait.

Quelque chose de très proche de cette définition finira certainement bien par arriver dans la bibliothèque standard de Rust. Pour le moment, cela fait partie de la boîte à outils de la plupart des moteurs d’exécution, vous pouvez donc vous baser dessus, et tout ce que nous allons voir devrait généralement pouvoir s’appliquer !

Cependant, dans les exemples que nous avons vus dans la section “Les flux : futures en séquence”, nous n’avions utilisé ni poll_next ni Stream, mais à la place, nous avions utilisé next et StreamExt. Bien entendu, nous pourrions travailler directement avec l’API poll_next en écrivant à la main notre propre machine à états Stream, tout comme nous pourrions travailler avec des futures directement en passant par leur méthode poll. Utiliser await est toutefois bien plus pratique, et le trait StreamExt fournit la méthode next, ce qui nous permet précisément de faire cela :

#![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;

    // autres méthodes...
}
}

Note : la définition que nous avons utilisée plus tôt dans ce chapitre est légèrement différente de celles-ci, car elle est compatible avec les versions de Rust qui ne prenaient pas encore en compte l’utilisation des fonctions asynchrones dans les traits. Elle se présente donc comme suit :

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

Ce type Next est un struct qui implémente Future et nous permet de nommer la durée de vie de la référence à self avec Next<'_, Self>, de sorte que await peut fonctionner avec cette méthode.

Le trait StreamExt est aussi l’endroit où se trouvent toutes les méthodes intéressantes pour les flux. StreamExt est automatiquement implémenté pour tout type qui implémente Stream, mais ces traits sont définis séparément, afin de permettre à la communauté de faire évoluer les APIs utiles sans pour autant interférer avec le trait de base.

Dans la version de StreamExt utilisée dans la crate trpl crate, non seulement le trait définit la méthode next, mais encore il fournit une implémentation par défaut de next qui gère correctement les détails de l’appel à Stream::poll_next. Ceci implique que même quand vous devez écrire votre propre type de données de flux, vous n’avez _qu’_à implémenter Stream, puis quiconque utilisera votre type de données pourra automatiquement utiliser StreamExt et ses méthodes associées.

C’est là tout ce que nous allons traiter concernant les détails de bas niveau de ces traits. Pour résumer, voyons comment les futures (en incluant les flux), les tâches et les fils d’exécution s’assemblent tous ensemble !