ExamplesBy LevelBy TopicLearning Paths
1019 Intermediate

1019-fallible-iterator — Fallible Iterator

Functional Programming

Tutorial

The Problem

Standard iterators yield T values with no error channel. But many real-world data sources — file readers, database cursors, network streams — can fail mid-iteration. The fallible-iterator crate and manual implementations model this with iterators that yield Option<Result<T, E>>: None for end-of-sequence, Some(Ok(v)) for a value, and Some(Err(e)) for a failure.

This is the basis of serde's streaming deserialisation, std::io::Lines, and async stream error handling in Tokio.

🎯 Learning Outcomes

  • • Implement an Iterator with Item = Result<T, E> for fallible sequences
  • • Use collect::<Result<Vec<T>, E>>() to consume a fallible iterator with fail-fast behaviour
  • • Build an adaptor that stops at the first error versus one that accumulates both sides
  • • Understand how std::io::Lines uses the same pattern
  • • Connect this pattern to async Stream types in Tokio
  • Code Example

    #![allow(clippy::all)]
    // 1019: Fallible Iterator
    // Iterator that can fail: next() -> Option<Result<T,E>>
    
    // Approach 1: Iterator yielding Result items
    struct LineParser {
        lines: Vec<String>,
        index: usize,
    }
    
    impl LineParser {
        fn new(lines: Vec<&str>) -> Self {
            LineParser {
                lines: lines.into_iter().map(String::from).collect(),
                index: 0,
            }
        }
    }
    
    impl Iterator for LineParser {
        type Item = Result<i64, String>;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.index >= self.lines.len() {
                return None;
            }
            let line = &self.lines[self.index];
            self.index += 1;
            Some(
                line.trim()
                    .parse::<i64>()
                    .map_err(|_| format!("bad line: {}", line)),
            )
        }
    }
    
    // Approach 2: Adaptor that stops at first error
    fn take_while_ok<T, E>(iter: impl Iterator<Item = Result<T, E>>) -> Result<Vec<T>, E> {
        let mut results = Vec::new();
        for item in iter {
            results.push(item?);
        }
        Ok(results)
    }
    
    // Approach 3: Process all, keeping partial results
    fn process_all<T, E>(iter: impl Iterator<Item = Result<T, E>>) -> (Vec<T>, Vec<E>) {
        let mut oks = Vec::new();
        let mut errs = Vec::new();
        for item in iter {
            match item {
                Ok(v) => oks.push(v),
                Err(e) => errs.push(e),
            }
        }
        (oks, errs)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result = take_while_ok(parser);
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_stops_at_error() {
            let parser = LineParser::new(vec!["1", "abc", "3"]);
            let result = take_while_ok(parser);
            assert!(result.is_err());
            assert!(result.unwrap_err().contains("bad line"));
        }
    
        #[test]
        fn test_empty_iterator() {
            let parser = LineParser::new(vec![]);
            assert_eq!(take_while_ok(parser), Ok(vec![]));
        }
    
        #[test]
        fn test_process_all_mixed() {
            let parser = LineParser::new(vec!["1", "abc", "3", "def"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    
        #[test]
        fn test_process_all_valid() {
            let parser = LineParser::new(vec!["10", "20"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![10, 20]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_collect_shortcircuit() {
            // Standard collect on Result also works
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result: Result<Vec<i64>, String> = parser.collect();
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_iterator_is_lazy() {
            let parser = LineParser::new(vec!["1", "bad", "3"]);
            // Take only first item — error not reached
            let first = parser.into_iter().next();
            assert_eq!(first, Some(Ok(1)));
        }
    }

    Key Differences

  • Type in std: Rust's std::io::Lines uses Iterator<Item=Result<String, io::Error>> in the standard library; OCaml's Seq.t is pure and requires manual wrapping.
  • **collect integration**: Rust's collect::<Result<Vec<T>, E>>() works out of the box; OCaml needs custom accumulation.
  • Async extension: Rust's fallible_iterator pattern maps directly to tokio_stream::Stream; OCaml's async equivalents (Lwt_stream) have similar but different APIs.
  • Early termination: Rust's for loop with ? inside is a first-class early-return mechanism; OCaml requires explicit recursion or a custom combinator.
  • OCaml Approach

    OCaml sequences (Seq.t) are lazy but not natively fallible. Fallible iteration requires wrapping:

    type 'a result_seq = unit -> ('a, exn) result Seq.node
    
    let take_while_ok seq =
      let rec go acc s =
        match s () with
        | Seq.Nil -> Ok (List.rev acc)
        | Seq.Cons (Ok v, rest) -> go (v :: acc) rest
        | Seq.Cons (Error e, _) -> Error e
      in
      go [] seq
    

    Libraries like Streaming provide Source.t with built-in error handling, mirroring Rust's fallible_iterator crate.

    Full Source

    #![allow(clippy::all)]
    // 1019: Fallible Iterator
    // Iterator that can fail: next() -> Option<Result<T,E>>
    
    // Approach 1: Iterator yielding Result items
    struct LineParser {
        lines: Vec<String>,
        index: usize,
    }
    
    impl LineParser {
        fn new(lines: Vec<&str>) -> Self {
            LineParser {
                lines: lines.into_iter().map(String::from).collect(),
                index: 0,
            }
        }
    }
    
    impl Iterator for LineParser {
        type Item = Result<i64, String>;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.index >= self.lines.len() {
                return None;
            }
            let line = &self.lines[self.index];
            self.index += 1;
            Some(
                line.trim()
                    .parse::<i64>()
                    .map_err(|_| format!("bad line: {}", line)),
            )
        }
    }
    
    // Approach 2: Adaptor that stops at first error
    fn take_while_ok<T, E>(iter: impl Iterator<Item = Result<T, E>>) -> Result<Vec<T>, E> {
        let mut results = Vec::new();
        for item in iter {
            results.push(item?);
        }
        Ok(results)
    }
    
    // Approach 3: Process all, keeping partial results
    fn process_all<T, E>(iter: impl Iterator<Item = Result<T, E>>) -> (Vec<T>, Vec<E>) {
        let mut oks = Vec::new();
        let mut errs = Vec::new();
        for item in iter {
            match item {
                Ok(v) => oks.push(v),
                Err(e) => errs.push(e),
            }
        }
        (oks, errs)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result = take_while_ok(parser);
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_stops_at_error() {
            let parser = LineParser::new(vec!["1", "abc", "3"]);
            let result = take_while_ok(parser);
            assert!(result.is_err());
            assert!(result.unwrap_err().contains("bad line"));
        }
    
        #[test]
        fn test_empty_iterator() {
            let parser = LineParser::new(vec![]);
            assert_eq!(take_while_ok(parser), Ok(vec![]));
        }
    
        #[test]
        fn test_process_all_mixed() {
            let parser = LineParser::new(vec!["1", "abc", "3", "def"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    
        #[test]
        fn test_process_all_valid() {
            let parser = LineParser::new(vec!["10", "20"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![10, 20]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_collect_shortcircuit() {
            // Standard collect on Result also works
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result: Result<Vec<i64>, String> = parser.collect();
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_iterator_is_lazy() {
            let parser = LineParser::new(vec!["1", "bad", "3"]);
            // Take only first item — error not reached
            let first = parser.into_iter().next();
            assert_eq!(first, Some(Ok(1)));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result = take_while_ok(parser);
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_stops_at_error() {
            let parser = LineParser::new(vec!["1", "abc", "3"]);
            let result = take_while_ok(parser);
            assert!(result.is_err());
            assert!(result.unwrap_err().contains("bad line"));
        }
    
        #[test]
        fn test_empty_iterator() {
            let parser = LineParser::new(vec![]);
            assert_eq!(take_while_ok(parser), Ok(vec![]));
        }
    
        #[test]
        fn test_process_all_mixed() {
            let parser = LineParser::new(vec!["1", "abc", "3", "def"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![1, 3]);
            assert_eq!(errs.len(), 2);
        }
    
        #[test]
        fn test_process_all_valid() {
            let parser = LineParser::new(vec!["10", "20"]);
            let (oks, errs) = process_all(parser);
            assert_eq!(oks, vec![10, 20]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_collect_shortcircuit() {
            // Standard collect on Result also works
            let parser = LineParser::new(vec!["1", "2", "3"]);
            let result: Result<Vec<i64>, String> = parser.collect();
            assert_eq!(result, Ok(vec![1, 2, 3]));
        }
    
        #[test]
        fn test_iterator_is_lazy() {
            let parser = LineParser::new(vec!["1", "bad", "3"]);
            // Take only first item — error not reached
            let first = parser.into_iter().next();
            assert_eq!(first, Some(Ok(1)));
        }
    }

    Deep Comparison

    Fallible Iterator — Comparison

    Core Insight

    Iterators that can fail at each step need two layers: "are there more items?" (Option/Seq) and "did this item succeed?" (Result). Both languages layer these naturally.

    OCaml Approach

  • Seq yields Result values: Seq.t (('a, 'e) result)
  • • Manual recursive processing with Seq.Cons/Seq.Nil
  • • Stateful iterator via mutable record
  • • No standard FallibleIterator abstraction
  • Rust Approach

  • Iterator<Item = Result<T, E>> — natural composition
  • collect::<Result<Vec<T>, E>>() for short-circuit collection
  • • Custom take_while_ok / process_all for different strategies
  • fallible-iterator crate for dedicated abstraction
  • Comparison Table

    AspectOCamlRust
    Type(T, E) result Seq.tIterator<Item=Result<T,E>>
    LazinessSeq is lazyIterator is lazy
    Short-circuitManual recursioncollect() or ? in loop
    All resultsManual foldpartition / custom
    StatefulMutable recordimpl Iterator with fields

    Exercises

  • Add a skip_errors method to LineParser that filters out Err items and only yields Ok values as a new iterator type.
  • Implement a FallibleZip iterator that zips two fallible iterators and returns Err if either source fails.
  • Write a function that reads lines from a BufReader<File> using Lines, parses each as an i64, and collects them with the fallible collect pattern.
  • Open Source Repos