ExamplesBy LevelBy TopicLearning Paths
864 Fundamental

Traverse with Result

Functional Programming

Tutorial

The Problem

When processing a list of inputs that might each fail with an error, you often want to either collect all successful results or report the first error. This is traverse for Result: Vec<T> → (T → Result<U,E>) → Result<Vec<U>,E>. Rust's Iterator::collect::<Result<Vec<T>,E>>() is exactly this — it short-circuits on the first Err and returns it, or collects all Ok values. Real applications: batch parsing user inputs, processing CSV rows (fail on first bad row), executing a list of database updates (rollback on first failure), and validating a sequence of configuration values.

🎯 Learning Outcomes

  • • Understand traverse for Result: short-circuits on first Err, collects all Ok on success
  • • Recognize Iterator::collect::<Result<Vec<U>,E>>() as the idiomatic Rust traverse for Result
  • • Implement using try_fold to understand the mechanics
  • • Distinguish traverse-Result from traverse-Option: Result carries error information; Option just signals absence
  • • Compare with Validated traversal: Result short-circuits; Validated accumulates all errors
  • Code Example

    fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
        xs.iter().map(f).collect()  // Built-in!
    }

    Key Differences

    AspectRustOCaml
    Idiomaticcollect::<Result<Vec<_>,_>>()List.fold_right
    Short-circuitFirst Err terminatesSame
    Error infoPreserved in Err(e)Preserved
    vs. ValidatedShort-circuitsAccumulates all errors
    Manualtry_fold with ?Result.bind in fold
    TypeResult<Vec<U>, E>('u list, 'e) result

    OCaml Approach

    OCaml's traverse for Result: let traverse f xs = List.fold_right (fun x acc -> match f x, acc with Ok y, Ok ys -> Ok (y :: ys) | Error e, _ -> Error e | _, Error e -> Error e) xs (Ok []). Alternatively using Result.bind: let traverse f xs = List.fold_left (fun acc x -> acc |> Result.bind (fun ys -> f x |> Result.map (fun y -> ys @ [y]))) (Ok []) xs. OCaml's let* y = f x in Ok (y :: ys) with let%bind reads cleanly.

    Full Source

    #![allow(clippy::all)]
    // Example 065: Traverse with Result
    // Turn Vec<Result<T,E>> into Result<Vec<T>,E>
    
    // Approach 1: Using collect (Rust's built-in traverse for Result!)
    fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
        xs.iter().map(f).collect()
    }
    
    // Approach 2: Using try_fold
    fn traverse_result_fold<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
        xs.iter().try_fold(Vec::new(), |mut acc, x| {
            acc.push(f(x)?);
            Ok(acc)
        })
    }
    
    // Approach 3: Sequence
    fn sequence_result<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
        xs.into_iter().collect()
    }
    
    fn parse_positive(s: &&str) -> Result<i32, String> {
        let n: i32 = s.parse().map_err(|_| format!("Not a number: {}", s))?;
        if n <= 0 {
            Err(format!("Not positive: {}", n))
        } else {
            Ok(n)
        }
    }
    
    fn validate_username(s: &&str) -> Result<String, String> {
        if s.len() < 3 {
            Err("Too short".into())
        } else if s.len() > 20 {
            Err("Too long".into())
        } else {
            Ok(s.to_string())
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_traverse_all_ok() {
            assert_eq!(
                traverse_result(&["1", "2", "3"], parse_positive),
                Ok(vec![1, 2, 3])
            );
        }
    
        #[test]
        fn test_traverse_parse_error() {
            assert_eq!(
                traverse_result(&["1", "bad", "3"], parse_positive),
                Err("Not a number: bad".into())
            );
        }
    
        #[test]
        fn test_traverse_validation_error() {
            assert_eq!(
                traverse_result(&["1", "-2", "3"], parse_positive),
                Err("Not positive: -2".into())
            );
        }
    
        #[test]
        fn test_traverse_empty() {
            let empty: &[&str] = &[];
            assert_eq!(traverse_result(empty, parse_positive), Ok(vec![]));
        }
    
        #[test]
        fn test_fold_version() {
            assert_eq!(
                traverse_result_fold(&["1", "2"], parse_positive),
                Ok(vec![1, 2])
            );
            assert_eq!(
                traverse_result_fold(&["1", "bad"], parse_positive),
                Err("Not a number: bad".into())
            );
        }
    
        #[test]
        fn test_sequence_ok() {
            assert_eq!(
                sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
                Ok(vec![1, 2, 3])
            );
        }
    
        #[test]
        fn test_sequence_err() {
            let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
            assert_eq!(sequence_result(rs), Err("e"));
        }
    
        #[test]
        fn test_validate_usernames() {
            assert_eq!(
                traverse_result(&["alice", "bob"], validate_username),
                Ok(vec!["alice".into(), "bob".into()])
            );
            assert_eq!(
                traverse_result(&["alice", "ab"], validate_username),
                Err("Too short".into())
            );
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_traverse_all_ok() {
            assert_eq!(
                traverse_result(&["1", "2", "3"], parse_positive),
                Ok(vec![1, 2, 3])
            );
        }
    
        #[test]
        fn test_traverse_parse_error() {
            assert_eq!(
                traverse_result(&["1", "bad", "3"], parse_positive),
                Err("Not a number: bad".into())
            );
        }
    
        #[test]
        fn test_traverse_validation_error() {
            assert_eq!(
                traverse_result(&["1", "-2", "3"], parse_positive),
                Err("Not positive: -2".into())
            );
        }
    
        #[test]
        fn test_traverse_empty() {
            let empty: &[&str] = &[];
            assert_eq!(traverse_result(empty, parse_positive), Ok(vec![]));
        }
    
        #[test]
        fn test_fold_version() {
            assert_eq!(
                traverse_result_fold(&["1", "2"], parse_positive),
                Ok(vec![1, 2])
            );
            assert_eq!(
                traverse_result_fold(&["1", "bad"], parse_positive),
                Err("Not a number: bad".into())
            );
        }
    
        #[test]
        fn test_sequence_ok() {
            assert_eq!(
                sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
                Ok(vec![1, 2, 3])
            );
        }
    
        #[test]
        fn test_sequence_err() {
            let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
            assert_eq!(sequence_result(rs), Err("e"));
        }
    
        #[test]
        fn test_validate_usernames() {
            assert_eq!(
                traverse_result(&["alice", "bob"], validate_username),
                Ok(vec!["alice".into(), "bob".into()])
            );
            assert_eq!(
                traverse_result(&["alice", "ab"], validate_username),
                Err("Too short".into())
            );
        }
    }

    Deep Comparison

    Comparison: Traverse with Result

    Traverse

    OCaml:

    let rec traverse_result f = function
      | [] -> Ok []
      | x :: xs ->
        match f x with
        | Error e -> Error e
        | Ok y -> match traverse_result f xs with
          | Error e -> Error e
          | Ok ys -> Ok (y :: ys)
    

    Rust:

    fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
        xs.iter().map(f).collect()  // Built-in!
    }
    

    Sequence

    OCaml:

    let sequence_result xs = traverse_result Fun.id xs
    

    Rust:

    fn sequence_result<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
        xs.into_iter().collect()
    }
    

    Exercises

  • Use traverse_result to parse a CSV line into typed fields, returning the first parse error with context.
  • Implement a batch database insert using traverse_result: insert all rows or return the first constraint violation.
  • Compare traverse_result (first error) with traverse_validated (all errors) on the same input and show the difference.
  • Implement traverse_result that accumulates ALL errors using Validated internally, then converts to Result.
  • Write property tests verifying that traverse_result(xs, f) is equivalent to xs.iter().map(f).collect().
  • Open Source Repos