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

Traverse and Sequence Patterns

Traversal maps a function over inputs and combines its results. Sequencing combines results or effects that have already been constructed.

Validation accumulates errors

traverse and sequence in this module operate on Validation. They preserve input order and combine errors using Semigroup.

use stillwater::{Validation, traverse::{traverse, sequence}};

fn positive(value: i32) -> Validation<i32, Vec<i32>> {
    if value > 0 { Validation::success(value) } else { Validation::failure(vec![value]) }
}

assert_eq!(traverse([1, 2, 3], positive), Validation::Success(vec![1, 2, 3]));
assert_eq!(traverse([1, -2, -3], positive), Validation::Failure(vec![-2, -3]));
assert_eq!(traverse([], positive), Validation::Success(vec![]));

let checks = [positive(-1), positive(2), positive(-3)];
assert_eq!(sequence(checks), Validation::Failure(vec![-1, -3]));

Sequential effect traversal

traverse_effect_sequential calls the factory for one item, awaits that child, and only then proceeds. After an error, later child effects are neither constructed nor run.

use std::sync::{Arc, Mutex};
use stillwater::prelude::*;

tokio_test::block_on(async {
    let constructed = Arc::new(Mutex::new(Vec::new()));
    let seen = Arc::clone(&constructed);
    let effect = traverse_effect_sequential([1, 2, 3], move |item| {
        seen.lock().unwrap().push(item);
        if item == 2 { fail("stop").boxed() } else { pure(item).boxed() }
    });
    assert!(constructed.lock().unwrap().is_empty());
    assert_eq!(effect.run(&()).await, Err("stop"));
    assert_eq!(*constructed.lock().unwrap(), vec![1, 2]);
});

Parallel effect traversal

traverse_effect_parallel constructs all children when the returned effect executes, then polls them concurrently. It waits for the entire batch, even after an error. Successful values retain input order; if multiple children fail, the error at the earliest input position is returned.

use stillwater::prelude::*;

tokio_test::block_on(async {
    let effect = traverse_effect_parallel([1, 2, 3], |item| {
        from_async(move |_: &()| async move {
            tokio::task::yield_now().await;
            Ok::<_, &'static str>(item * 2)
        }).boxed()
    });
    assert_eq!(effect.run(&()).await, Ok(vec![2, 4, 6]));
});

Concurrent polling does not move CPU work onto separate threads. A child that blocks its poll can block progress for the whole batch. For large I/O batches, use par_all_limit or chunking to bound concurrency. Neither parallel traversal variant provides rollback or keeps children running after the parent future is dropped.

Sequence existing effects

use stillwater::prelude::*;

tokio_test::block_on(async {
    let ordered = sequence_effect_sequential(vec![
        pure::<_, &str, ()>(1).boxed(),
        pure(2).boxed(),
    ]);
    assert_eq!(ordered.run(&()).await, Ok(vec![1, 2]));

    let concurrent = sequence_effect_parallel(vec![
        fail::<i32, _, ()>("first input").boxed(),
        fail("second input").boxed(),
    ]);
    assert_eq!(concurrent.run(&()).await, Err("first input"));
});

Sequential sequencing stops running children after the first error, but their construction has already happened. Use traversal when child construction must also be deferred.

Construction, memory, and testing

All four effect helpers eagerly collect their input iterator when called. Side effects in an iterator’s next method therefore occur before execution. Traversal defers the supplied child factory, not input enumeration. Inputs must be finite.

The effect helpers return BoxedEffect, require boxed children, and clone the environment for execution. Prefer cheap shared service handles in the environment. Validation traversal also materializes an intermediate vector; it makes no single-allocation or speedup guarantee over a manual map and sequence.

Regression tests in src/traverse.rs cover child construction timing, strict sequential ordering, actual overlap, empty inputs, input-ordered errors despite out-of-order completion, and completion after errors. Use synchronization to control completion order in tests instead of assuming timer precision.

For the 1.x behavior and upgrade details, see migration.