Crate first_err

source ·
Expand description

first-err

Find the first Err in Iterator<Item = Result<T, E>> and allow iterating continuously.

This crate is specifically designed to replace the following pattern without allocation:

// iter: impl Iterator<Item = Result<T, E>>
iter.collect::<Result<Vec<T>, E>>().map(|vec| vec.into_iter().foo() );

See FirstErr trait for more detail.

Features

  • Easy-to-use: simple and no way to using wrong.
  • Minimized: no std, no alloc, zero dependency.
  • Fast: Roughly on par with a hand-written loop, using lazy evaluation and no allocation.
  • Nestable: T in Iterator<Item = Result<T, E>> can lazily produce more Results.

Getting Started

// Use this trait in current scope.
use first_err::FirstErr;

// Everything is Ok.
let result = [Ok::<u8, u8>(0), Ok(1), Ok(2)]
    .into_iter()
    .first_err_or_else(|iter| iter.sum::<u8>());
assert_eq!(result, Ok(3));

// Contains some `Err` values.
let result = [Ok::<u8, u8>(0), Err(1), Err(2)]
    .into_iter()
    .first_err_or_else(|iter| iter.sum::<u8>());
assert_eq!(result, Err(1));

Why

In Rust, I frequently encounter a pattern where I need to perform actions on all items within an iterator, and halt immediately if any error is detected in the layer I’m working on. But if no error found, the iterator should able to run continuously and allow me to do further transform.

The pattern typically looks as follows:

let array: [Result<u8, u8>; 3] = [Ok(0), Err(1), Err(2)];

fn fallible_sum(iter: impl IntoIterator<Item = Result<u8, u8>>) -> Result<u8, u8> {
    let sum = iter
        .into_iter()
        .collect::<Result<Vec<_>, _>>()?    // early return (and a vec alloc in here)
        .into_iter()                        // continue iterate next layer ...
        .sum();

    Ok(sum)
}

let result = fallible_sum(array);
assert_eq!(result, Err(1));

In theory, this allocation is not necessary. We can just write that code as an old good loop:

let array: [Result<u8, u8>; 3] = [Ok(0), Err(1), Err(2)];

fn fallible_sum(iter: impl IntoIterator<Item = Result<u8, u8>>) -> Result<u8, u8> {
    let mut sum = 0;
    for res in iter {
        let val = res?;                     // early return, no alloc
        sum += val;
    }

    Ok(sum)
}

let result = fallible_sum(array);
assert_eq!(result, Err(1))

Using a loop is not bad at all. However, in some situations, maintaining a chainable iterator is preferable.

Furthermore, some scenarios may not be as simple as the previous example. Consider this one:

// The second layer `Result` is usually created by further transform after the first layer
// `Result` be processed. But for the sake of simplicity, we've just use pre-defined values.
let array: [Result<Result<u8, u8>, u8>; 3] = [Ok(Ok(0)), Ok(Err(1)), Err(2)];

fn fallible_sum(
    iter: impl IntoIterator<Item = Result<Result<u8, u8>, u8>>
) -> Result<u8, u8> {
    // take "first `Err`" layer by layer, or the sum value.
    let sum = iter
        .into_iter()
        .collect::<Result<Vec<Result<u8, u8>>, u8>>()?
        .into_iter()
        .collect::<Result<Vec<u8>, u8>>()?
        .into_iter()
        .sum();

    Ok(sum)
}

let result = fallible_sum(array);
assert_eq!(result, Err(2));

Implementing the above logic in a loop without allocation may be error-prone and complicated. This crate simplifies that for you:

let array: [Result<Result<u8, u8>, u8>; 3] = [Ok(Ok(0)), Ok(Err(1)), Err(2)];

fn fallible_sum(
    iter: impl IntoIterator<Item = Result<Result<u8, u8>, u8>>
) -> Result<u8, u8> {
    iter
        .into_iter()
        .first_err_or_try(|iter1| { // iter1 = impl Iterator<Item = Result<u8, u8>>
            iter1.first_err_or_else(|iter2| { // iter2 = impl Iterator<Item = u8>
                iter2.sum::<u8>()
            })
        })
}

let result = fallible_sum(array);
assert_eq!(result, Err(2));

Performance

The performance of this crate is designed to be roughly on par with hand-written loops. However, the compiler might apply different optimizations in various situations, and favoring one approach over the others.

If you want to to do a benchmark by yourself, use the following command:

cargo bench --bench benchmark -- --output-format bencher

Also, don’t forget to check the actual code that is used for benchmarking, which is in the benches folder.

Structs

  • An Iterator can take first Err from another iterator.
  • An Iterator can take first None from another iterator.

Traits

  • This trait provides some methods on any Iterator<Item = Result<T, E>>, which can take the first Err in iterators, and without allocation.