1019-fallible-iterator — Fallible Iterator
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
Iterator with Item = Result<T, E> for fallible sequencescollect::<Result<Vec<T>, E>>() to consume a fallible iterator with fail-fast behaviourstd::io::Lines uses the same patternStream types in TokioCode 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
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.fallible_iterator pattern maps directly to tokio_stream::Stream; OCaml's async equivalents (Lwt_stream) have similar but different APIs.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)));
}
}#[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)Seq.Cons/Seq.NilFallibleIterator abstractionRust Approach
Iterator<Item = Result<T, E>> — natural compositioncollect::<Result<Vec<T>, E>>() for short-circuit collectiontake_while_ok / process_all for different strategiesfallible-iterator crate for dedicated abstractionComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Type | (T, E) result Seq.t | Iterator<Item=Result<T,E>> |
| Laziness | Seq is lazy | Iterator is lazy |
| Short-circuit | Manual recursion | collect() or ? in loop |
| All results | Manual fold | partition / custom |
| Stateful | Mutable record | impl Iterator with fields |
Exercises
skip_errors method to LineParser that filters out Err items and only yields Ok values as a new iterator type.FallibleZip iterator that zips two fallible iterators and returns Err if either source fails.BufReader<File> using Lines, parses each as an i64, and collects them with the fallible collect pattern.