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
, noalloc
, zero dependency. - Fast: Roughly on par with a hand-written loop, using lazy evaluation and no allocation.
- Nestable:
T
inIterator<Item = Result<T, E>>
can lazily produce moreResult
s.
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 firstErr
from another iterator. - An
Iterator
can take firstNone
from another iterator.
Traits
- This trait provides some methods on any
Iterator<Item = Result<T, E>>
, which can take the firstErr
in iterators, and without allocation.