1020-try-fold — try_fold
Tutorial
The Problem
fold (reduce) is the fundamental operation of functional programming: reduce a sequence to a single value by combining elements with an accumulator. When the combining function is fallible — parsing, validation, arithmetic with overflow — you need a variant that short-circuits on the first error.
Iterator::try_fold is Rust's answer. It combines the ergonomics of ? with the power of fold, stopping as soon as the combining function returns Err. This is the engine behind Iterator::try_for_each, Iterator::sum (for types that can overflow), and the collect::<Result<Vec<_>, _>>() implementation.
🎯 Learning Outcomes
Iterator::try_fold to fold with short-circuit on errortry_fold relates to fold and explicit for/? loopstry_fold to accumulate state while validating inputstry_fold as the primitive that collect::<Result<...>> is built onchecked_* methods inside try_foldCode Example
#![allow(clippy::all)]
// 1020: try_fold — Fold that short-circuits on error
// Approach 1: Iterator::try_fold
fn sum_positive(numbers: &[i64]) -> Result<i64, String> {
numbers.iter().try_fold(0i64, |acc, &n| {
if n < 0 {
Err(format!("negative number: {}", n))
} else {
Ok(acc + n)
}
})
}
// Approach 2: try_fold with accumulator transformation
fn concat_limited(strings: &[&str], max_len: usize) -> Result<String, String> {
strings.iter().try_fold(String::new(), |mut acc, &s| {
acc.push_str(s);
if acc.len() > max_len {
Err(format!("result too long: {} > {}", acc.len(), max_len))
} else {
Ok(acc)
}
})
}
// Approach 3: try_fold vs regular fold comparison
fn product_no_overflow(numbers: &[i64]) -> Result<i64, String> {
numbers.iter().try_fold(1i64, |acc, &n| {
acc.checked_mul(n)
.ok_or_else(|| format!("overflow at {} * {}", acc, n))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_all_positive() {
assert_eq!(sum_positive(&[1, 2, 3]), Ok(6));
}
#[test]
fn test_sum_negative_fails() {
let result = sum_positive(&[1, -2, 3]);
assert_eq!(result, Err("negative number: -2".to_string()));
}
#[test]
fn test_sum_empty() {
assert_eq!(sum_positive(&[]), Ok(0));
}
#[test]
fn test_concat_ok() {
assert_eq!(
concat_limited(&["hello", " ", "world"], 20),
Ok("hello world".to_string())
);
}
#[test]
fn test_concat_too_long() {
let result = concat_limited(&["hello", " ", "world!!!!!!!!!!!!"], 10);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too long"));
}
#[test]
fn test_product_ok() {
assert_eq!(product_no_overflow(&[2, 3, 4]), Ok(24));
}
#[test]
fn test_product_overflow() {
let result = product_no_overflow(&[i64::MAX, 2]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("overflow"));
}
#[test]
fn test_short_circuit_proof() {
// try_fold stops processing after first error
let mut count = 0;
let result = [1, -2, 3, 4, 5].iter().try_fold(0, |acc, &n| {
count += 1;
if n < 0 {
Err("negative")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
assert_eq!(count, 2); // only processed [1, -2], stopped
}
#[test]
fn test_try_fold_vs_fold() {
// Regular fold processes everything
let sum = [1, 2, 3].iter().fold(0, |acc, n| acc + n);
assert_eq!(sum, 6);
// try_fold can bail early
let result = [1, 2, 3].iter().try_fold(0, |acc, &n| {
if acc + n > 4 {
Err("too big")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
}
}Key Differences
try_fold stops the iterator immediately on Err; the OCaml fold-with-accumulator pattern visits all elements even after failure.try_fold short-circuits work correctly; OCaml lists are strict so the loop always runs to completion.Try trait integration**: Rust's try_fold works with any type implementing Try (including Option); OCaml's manual approach requires separate versions per monad.checked_* methods**: Rust has checked_add, checked_mul, etc. returning Option; OCaml uses exception-based overflow detection or the Zarith library.OCaml Approach
OCaml's equivalent is List.fold_left with an explicit Result accumulator:
let try_fold f init list =
List.fold_left (fun acc x ->
match acc with
| Error _ -> acc
| Ok state -> f state x
) (Ok init) list
This continues iterating even after an error (just passing the error through), which is slightly less efficient than Rust's early termination. A fully lazy version requires a recursive approach.
Full Source
#![allow(clippy::all)]
// 1020: try_fold — Fold that short-circuits on error
// Approach 1: Iterator::try_fold
fn sum_positive(numbers: &[i64]) -> Result<i64, String> {
numbers.iter().try_fold(0i64, |acc, &n| {
if n < 0 {
Err(format!("negative number: {}", n))
} else {
Ok(acc + n)
}
})
}
// Approach 2: try_fold with accumulator transformation
fn concat_limited(strings: &[&str], max_len: usize) -> Result<String, String> {
strings.iter().try_fold(String::new(), |mut acc, &s| {
acc.push_str(s);
if acc.len() > max_len {
Err(format!("result too long: {} > {}", acc.len(), max_len))
} else {
Ok(acc)
}
})
}
// Approach 3: try_fold vs regular fold comparison
fn product_no_overflow(numbers: &[i64]) -> Result<i64, String> {
numbers.iter().try_fold(1i64, |acc, &n| {
acc.checked_mul(n)
.ok_or_else(|| format!("overflow at {} * {}", acc, n))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_all_positive() {
assert_eq!(sum_positive(&[1, 2, 3]), Ok(6));
}
#[test]
fn test_sum_negative_fails() {
let result = sum_positive(&[1, -2, 3]);
assert_eq!(result, Err("negative number: -2".to_string()));
}
#[test]
fn test_sum_empty() {
assert_eq!(sum_positive(&[]), Ok(0));
}
#[test]
fn test_concat_ok() {
assert_eq!(
concat_limited(&["hello", " ", "world"], 20),
Ok("hello world".to_string())
);
}
#[test]
fn test_concat_too_long() {
let result = concat_limited(&["hello", " ", "world!!!!!!!!!!!!"], 10);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too long"));
}
#[test]
fn test_product_ok() {
assert_eq!(product_no_overflow(&[2, 3, 4]), Ok(24));
}
#[test]
fn test_product_overflow() {
let result = product_no_overflow(&[i64::MAX, 2]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("overflow"));
}
#[test]
fn test_short_circuit_proof() {
// try_fold stops processing after first error
let mut count = 0;
let result = [1, -2, 3, 4, 5].iter().try_fold(0, |acc, &n| {
count += 1;
if n < 0 {
Err("negative")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
assert_eq!(count, 2); // only processed [1, -2], stopped
}
#[test]
fn test_try_fold_vs_fold() {
// Regular fold processes everything
let sum = [1, 2, 3].iter().fold(0, |acc, n| acc + n);
assert_eq!(sum, 6);
// try_fold can bail early
let result = [1, 2, 3].iter().try_fold(0, |acc, &n| {
if acc + n > 4 {
Err("too big")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_all_positive() {
assert_eq!(sum_positive(&[1, 2, 3]), Ok(6));
}
#[test]
fn test_sum_negative_fails() {
let result = sum_positive(&[1, -2, 3]);
assert_eq!(result, Err("negative number: -2".to_string()));
}
#[test]
fn test_sum_empty() {
assert_eq!(sum_positive(&[]), Ok(0));
}
#[test]
fn test_concat_ok() {
assert_eq!(
concat_limited(&["hello", " ", "world"], 20),
Ok("hello world".to_string())
);
}
#[test]
fn test_concat_too_long() {
let result = concat_limited(&["hello", " ", "world!!!!!!!!!!!!"], 10);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too long"));
}
#[test]
fn test_product_ok() {
assert_eq!(product_no_overflow(&[2, 3, 4]), Ok(24));
}
#[test]
fn test_product_overflow() {
let result = product_no_overflow(&[i64::MAX, 2]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("overflow"));
}
#[test]
fn test_short_circuit_proof() {
// try_fold stops processing after first error
let mut count = 0;
let result = [1, -2, 3, 4, 5].iter().try_fold(0, |acc, &n| {
count += 1;
if n < 0 {
Err("negative")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
assert_eq!(count, 2); // only processed [1, -2], stopped
}
#[test]
fn test_try_fold_vs_fold() {
// Regular fold processes everything
let sum = [1, 2, 3].iter().fold(0, |acc, n| acc + n);
assert_eq!(sum, 6);
// try_fold can bail early
let result = [1, 2, 3].iter().try_fold(0, |acc, &n| {
if acc + n > 4 {
Err("too big")
} else {
Ok(acc + n)
}
});
assert!(result.is_err());
}
}
Deep Comparison
try_fold — Comparison
Core Insight
Regular fold processes all elements. try_fold stops at the first failure — essential for validation pipelines and overflow-safe arithmetic.
OCaml Approach
try_fold — must write recursive versionOk/Error at each stepSeq for lazy evaluation with short-circuitRust Approach
Iterator::try_fold is built-in and optimizedResult<Acc, E> — Err short-circuitsOption (None short-circuits)Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Built-in | No | Yes (try_fold) |
| Short-circuit | Manual recursion | Automatic |
| Works with | Result (manual) | Result, Option, ControlFlow |
| Performance | Recursive | Optimized iterator machinery |
| Related | fold_left | fold, try_for_each |
Exercises
try_fold_with_index function that wraps try_fold to pass the current index along with each element to the combining function.parse_csv_row(row: &str, expected_cols: usize) -> Result<Vec<i64>, String> using try_fold to parse each comma-separated field.try_fold to implement all(pred) -> Result<bool, E> for a fallible predicate — returning Err if any predicate call fails, Ok(true) if all pass, Ok(false) if any return false.