ExamplesBy LevelBy TopicLearning Paths
1020 Intermediate

1020-try-fold — try_fold

Functional Programming

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

  • • Use Iterator::try_fold to fold with short-circuit on error
  • • Understand how try_fold relates to fold and explicit for/? loops
  • • Apply try_fold to accumulate state while validating inputs
  • • Recognize try_fold as the primitive that collect::<Result<...>> is built on
  • • Handle overflow-safe arithmetic using checked_* methods inside try_fold
  • Code 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

  • Early termination: Rust's try_fold stops the iterator immediately on Err; the OCaml fold-with-accumulator pattern visits all elements even after failure.
  • Lazy vs strict: Rust iterators are lazy so 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • • No built-in try_fold — must write recursive version
  • • Pattern match on Ok/Error at each step
  • • Can use Seq for lazy evaluation with short-circuit
  • Rust Approach

  • Iterator::try_fold is built-in and optimized
  • • Returns Result<Acc, E> — Err short-circuits
  • • Also works with Option (None short-circuits)
  • • Compiler can optimize the early-exit path
  • Comparison Table

    AspectOCamlRust
    Built-inNoYes (try_fold)
    Short-circuitManual recursionAutomatic
    Works withResult (manual)Result, Option, ControlFlow
    PerformanceRecursiveOptimized iterator machinery
    Relatedfold_leftfold, try_for_each

    Exercises

  • Write a try_fold_with_index function that wraps try_fold to pass the current index along with each element to the combining function.
  • Implement parse_csv_row(row: &str, expected_cols: usize) -> Result<Vec<i64>, String> using try_fold to parse each comma-separated field.
  • Use 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.
  • Open Source Repos