Skip to content

← Errors Are Values step 22 of 24

Medium Primitives

Collecting an iterator of Results

Parse the same list twice — once fail-fast, once forgiving — and prove the fail-fast pass stopped early.

pub fn strict_and_lenient(raw: Vec<String>) -> (Result<Vec<i32>, String>, Vec<i32>, usize)
  1. strict: Result<Vec<i32>, String> — all the values, or the first failure as "bad input: {original}".
  2. lenient: Vec<i32> — every value that parsed, skipping the rest.
  3. calls: how many times the strict pass’s closure ran.

For ["1", "2", "x", "4", "5"] the answers are Err("bad input: x"), [1, 2, 4, 5], and 3. Not 5. That third number is the whole point.

The best trick in this cluster

The standard library contains this impl:

impl<A, E, V: FromIterator<A>> FromIterator<Result<A, E>> for Result<V, E>

Read it slowly. If you can collect As into a V, then you can collect Result<A, E>s into a Result<V, E>. So an iterator of results collects into one result:

let strict: Result<Vec<i32>, String> = raw
    .iter()
    .map(|s| s.trim().parse::<i32>().map_err(|_| format!("bad input: {s}")))
    .collect();

One line, and it short-circuits: collect stops pulling from the iterator at the first Err, which is why the closure runs three times and not five. That replaces a loop, an accumulator vector, an error slot, an if first_error.is_none(), and a final match — all of which the starter contains.

The same impl exists for Option:

let all: Option<Vec<i32>> = xs.iter().map(|s| s.parse().ok()).collect();
// None if any element was None

This is nearly impossible to discover by browsing the method list, because it is not a method — it is a trait impl on the target type. Once you know it exists you will use it constantly.

The lenient half

let lenient: Vec<i32> = raw.iter().filter_map(|s| s.trim().parse::<i32>().ok()).collect();

filter_map keeps the Somes and drops the Nones in one pass. Do not write .filter(|r| r.is_ok()).map(|r| r.unwrap()) — clippy has iter_filter_is_ok and iter_filter_is_some for exactly that, and the unwrap is a panic waiting for a refactor. .flatten() works too: Result and Option are both iterators of at most one item.

The type annotation is usually mandatory

let parsed = raw.iter().map(|s| s.parse::<i32>()).collect();
error[E0283]: type annotations needed
   |
   |     let parsed = ... .collect();
   |         ^^^^^^                    type must be known at this point
   = note: cannot satisfy `_: FromIterator<Result<i32, ParseIntError>>`

collect is generic over its output, and there are many types it could build — Vec<Result<_,_>>, Result<Vec<_>,_>, HashSet<_>, and so on. Rust infers the return type from context when it can; when it cannot you get this “type annotations needed” error and must say which. Either annotate the binding (as above) or turbofish the call: .collect::<Result<Vec<_>, _>>().

This is also the reason the two collects in this problem behave so differently despite looking almost identical: the annotation is doing the work.

The neighbours

// sum and product also short-circuit through Result
let total: Result<i32, String> = items.iter().map(parse).sum();

// try_fold / try_for_each: short-circuiting folds
let n = items.iter().try_fold(0i32, |acc, x| acc.checked_add(*x).ok_or("overflow"))?;

And one trap. partition does not split results into values and errors:

let v: Vec<Result<i32, String>> = vec![Ok(1), Err("x".into()), Ok(3)];
let (a, b): (Vec<_>, Vec<_>) = v.into_iter().partition(Result::is_ok);
// a == [Ok(1), Ok(3)]      NOT [1, 3]
// b == [Err("x")]          NOT ["x"]

partition splits a collection into two collections of the same element type. Getting (Vec<i32>, Vec<String>) needs a fold, or two passes with filter_map.

Notes

  • The starter passes clippy and compiles cleanly. It is blocked purely by the calls count — which is the honest gate here, because “did it stop early?” is a behavioural property no lint can see.
  • An empty input collects to Ok(vec![]). Emptiness is not failure.
  • needless_collect (nursery) fires when you collect into a Vec only to immediately iterate it again. Worth knowing once your chains get longer.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…