ExamplesBy LevelBy TopicLearning Paths
320 Intermediate

320: Fallible Iterators

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "320: Fallible Iterators" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Iterators over external data sources — file lines, network streams, database cursors — may fail during iteration. Key difference from OCaml: 1. **Iterator type**: Rust iterators yielding `Result<T, E>` compose naturally with all iterator adapters; OCaml requires explicit fold

Tutorial

The Problem

Iterators over external data sources — file lines, network streams, database cursors — may fail during iteration. The standard Iterator trait doesn't accommodate per-element errors. The solution is an iterator yielding Result<T, E> items, combined with the collect::<Result<Vec<_>, _>>() short-circuit pattern or filter_map(Result::ok) for best-effort collection. This is the standard pattern for parsing streams and processing external data.

🎯 Learning Outcomes

  • • Implement iterators that yield Result<T, E> items for fallible element sources
  • • Use collect::<Result<Vec<T>, E>>() for fail-fast batch processing
  • • Use filter_map(|r| r.ok()) for best-effort processing that ignores errors
  • • Understand the tradeoffs: fail-fast vs best-effort vs error collection
  • Code Example

    #![allow(clippy::all)]
    //! # Fallible Iterator
    //!
    //! Iterators over Results with collect short-circuiting and best-effort patterns.
    
    /// Parse a single integer
    pub fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("cannot parse: {s}"))
    }
    
    /// Parse all - short-circuits on first error
    pub fn parse_all(inputs: &[&str]) -> Result<Vec<i64>, String> {
        inputs.iter().map(|s| parse_int(s)).collect()
    }
    
    /// Parse best effort - skip errors
    pub fn parse_best_effort(inputs: &[&str]) -> Vec<i64> {
        inputs.iter().filter_map(|s| parse_int(s).ok()).collect()
    }
    
    /// Parse with fallback value for errors
    pub fn parse_with_default(inputs: &[&str], default: i64) -> Vec<i64> {
        inputs
            .iter()
            .map(|s| parse_int(s).unwrap_or(default))
            .collect()
    }
    
    /// Partition into successes and failures
    pub fn parse_partition(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        let (oks, errs): (Vec<_>, Vec<_>) =
            inputs.iter().map(|s| parse_int(s)).partition(Result::is_ok);
        let nums: Vec<i64> = oks.into_iter().map(Result::unwrap).collect();
        let errors: Vec<String> = errs.into_iter().map(Result::unwrap_err).collect();
        (nums, errors)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_short_circuits() {
            assert!(parse_all(&["1", "bad", "3"]).is_err());
        }
    
        #[test]
        fn test_best_effort() {
            assert_eq!(parse_best_effort(&["1", "bad", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_empty_ok() {
            assert_eq!(parse_all(&[]), Ok(vec![]));
        }
    
        #[test]
        fn test_with_default() {
            assert_eq!(parse_with_default(&["1", "bad", "3"], 0), vec![1, 0, 3]);
        }
    
        #[test]
        fn test_partition() {
            let (nums, errs) = parse_partition(&["1", "bad", "3", "oops"]);
            assert_eq!(nums, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    }

    Key Differences

  • Iterator type: Rust iterators yielding Result<T, E> compose naturally with all iterator adapters; OCaml requires explicit fold-based handling.
  • Short-circuit collect: collect::<Result<Vec<T>, E>>() is the idiomatic Rust one-liner for fail-fast; OCaml requires explicit fold.
  • Fallible iterator crate: The fallible-iterator crate provides a FallibleIterator trait with map_err, and_then, and collect for cleaner error handling on stream-like sources.
  • Stream sources: BufRead::lines() returns Iterator<Item = io::Result<String>> — the standard library models fallible iteration this way.
  • OCaml Approach

    OCaml handles this with Seq.filter_map for best-effort and List.fold_right for fail-fast:

    (* Best-effort: *)
    let parse_best_effort inputs =
      Seq.filter_map (fun s -> int_of_string_opt s) (List.to_seq inputs)
      |> List.of_seq
    
    (* Fail-fast: *)
    let parse_all inputs =
      List.fold_right (fun s acc ->
        let* lst = acc in
        match int_of_string_opt s with
        | None -> Error ("not a number: " ^ s)
        | Some n -> Ok (n :: lst)
      ) inputs (Ok [])
    

    Full Source

    #![allow(clippy::all)]
    //! # Fallible Iterator
    //!
    //! Iterators over Results with collect short-circuiting and best-effort patterns.
    
    /// Parse a single integer
    pub fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("cannot parse: {s}"))
    }
    
    /// Parse all - short-circuits on first error
    pub fn parse_all(inputs: &[&str]) -> Result<Vec<i64>, String> {
        inputs.iter().map(|s| parse_int(s)).collect()
    }
    
    /// Parse best effort - skip errors
    pub fn parse_best_effort(inputs: &[&str]) -> Vec<i64> {
        inputs.iter().filter_map(|s| parse_int(s).ok()).collect()
    }
    
    /// Parse with fallback value for errors
    pub fn parse_with_default(inputs: &[&str], default: i64) -> Vec<i64> {
        inputs
            .iter()
            .map(|s| parse_int(s).unwrap_or(default))
            .collect()
    }
    
    /// Partition into successes and failures
    pub fn parse_partition(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        let (oks, errs): (Vec<_>, Vec<_>) =
            inputs.iter().map(|s| parse_int(s)).partition(Result::is_ok);
        let nums: Vec<i64> = oks.into_iter().map(Result::unwrap).collect();
        let errors: Vec<String> = errs.into_iter().map(Result::unwrap_err).collect();
        (nums, errors)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_short_circuits() {
            assert!(parse_all(&["1", "bad", "3"]).is_err());
        }
    
        #[test]
        fn test_best_effort() {
            assert_eq!(parse_best_effort(&["1", "bad", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_empty_ok() {
            assert_eq!(parse_all(&[]), Ok(vec![]));
        }
    
        #[test]
        fn test_with_default() {
            assert_eq!(parse_with_default(&["1", "bad", "3"], 0), vec![1, 0, 3]);
        }
    
        #[test]
        fn test_partition() {
            let (nums, errs) = parse_partition(&["1", "bad", "3", "oops"]);
            assert_eq!(nums, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_short_circuits() {
            assert!(parse_all(&["1", "bad", "3"]).is_err());
        }
    
        #[test]
        fn test_best_effort() {
            assert_eq!(parse_best_effort(&["1", "bad", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_empty_ok() {
            assert_eq!(parse_all(&[]), Ok(vec![]));
        }
    
        #[test]
        fn test_with_default() {
            assert_eq!(parse_with_default(&["1", "bad", "3"], 0), vec![1, 0, 3]);
        }
    
        #[test]
        fn test_partition() {
            let (nums, errs) = parse_partition(&["1", "bad", "3", "oops"]);
            assert_eq!(nums, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    }

    Deep Comparison

    fallible-iterator

    See README.md for details.

    Exercises

  • Implement a CSV line parser iterator that yields Result<Row, ParseError> per line, then collect all lines or fail on the first parse error.
  • Process a stream of log lines where some are malformed, using filter_map to skip malformed lines and count how many were skipped.
  • Implement parse_all_errors(inputs: &[&str]) -> (Vec<i64>, Vec<String>) that collects both successes and error messages in a single pass.
  • Open Source Repos