๐ŸŽ“ A1Learner
๐Ÿ”

Rust Stream Adapters in Practice

A stream adapter is a method that takes a stream and returns a changed stream. Most adapters copy iterator adapters, name for name.

skip drops the first items

skip(2) throws the first two items away. The stream starts at the third.

let mut s = stream::iter(vec![10, 20, 30, 40]).skip(2);
println!("third = {:?}", s.next().await);

take_while ends the stream early

take_while tests each item. The first failure ends the stream for good. The 8 after the 7 never flows.

let kept: Vec<i32> = stream::iter(vec![2, 4, 6, 7, 8])
    .take_while(|n| future::ready(*n % 2 == 0))
    .collect()
    .await;
println!("kept = {kept:?}");

filter_map changes and drops in one step

The closure returns a future of an Option. Some keeps the new value. None drops the item.

let words = vec!["10", "oops", "30"];
let nums: Vec<i32> = stream::iter(words)
    .filter_map(|w| async move { w.parse::<i32>().ok() })
    .collect()
    .await;
println!("nums = {nums:?}");

then is map with an await inside

Each item waits for its future before the next one starts. Order survives. Speed does not multiply.

let doubled: Vec<i32> = stream::iter(vec![1, 2, 3])
    .then(|n| async move { n * 2 })
    .collect()
    .await;
println!("doubled = {doubled:?}");

fold rolls the stream into one value

fold is a consumer, like collect. It pulls every item. It hands back one running total.

let total: i32 = stream::iter(vec![1, 2, 3, 4])
    .fold(0, |acc, n| async move { acc + n })
    .await;
println!("total = {total}");

Example

// Cargo.toml: futures = "0.3"
use futures::executor::block_on;
use futures::future;
use futures::stream::{self, StreamExt};

async fn run() {
    let readings = vec!["12", "skip-me", "7", "20", "3"];

    let total: i32 = stream::iter(readings)
        .filter_map(|w| async move { w.parse::<i32>().ok() })
        .take_while(|n| future::ready(*n < 20))
        .fold(0, |acc, n| async move { acc + n })
        .await;

    println!("total = {total}");
}

fn main() {
    block_on(run());
}

Key points

  • Use skip(n) to drop the first n items of a stream
  • Use take_while to end a stream at the first item that fails the test
  • Use filter_map when each item may change, vanish, or both
  • Use then when the change itself needs an await: one item at a time, in order
  • fold and collect are consumers: they pull the stream to its end and return one value
  • An adapter reads one stream and produces another stream from it.
  • The futures StreamExt holds the big catalogue.
  • tokio-stream StreamExt holds a smaller set.
  • A few adapters have no iterator twin at all.

In this example, five readings arrive as text. filter_map parses each one. The word "skip-me" fails to parse. It becomes None and drops out. take_while then keeps numbers under 20. The 20 stops the stream. The 3 behind it never arrives. fold adds what survives: 12 + 7 = 19.

Note

tokio-stream StreamExt adds time tools instead, like timeout and throttle.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

take_while meets its first failing item. Later items would pass the test again. What flows after the failure?