320: Fallible Iterators
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
Result<T, E> items for fallible element sourcescollect::<Result<Vec<T>, E>>() for fail-fast batch processingfilter_map(|r| r.ok()) for best-effort processing that ignores errorsCode 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
Result<T, E> compose naturally with all iterator adapters; OCaml requires explicit fold-based handling.collect::<Result<Vec<T>, E>>() is the idiomatic Rust one-liner for fail-fast; OCaml requires explicit fold.fallible-iterator crate provides a FallibleIterator trait with map_err, and_then, and collect for cleaner error handling on stream-like sources.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);
}
}#[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
Result<Row, ParseError> per line, then collect all lines or fail on the first parse error.filter_map to skip malformed lines and count how many were skipped.parse_all_errors(inputs: &[&str]) -> (Vec<i64>, Vec<String>) that collects both successes and error messages in a single pass.