ExamplesBy LevelBy TopicLearning Paths
314 Advanced

314: Validated — Accumulating All Errors

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "314: Validated — Accumulating All Errors" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. User registration forms, configuration validation, and batch processing all share a need: show all errors at once, not just the first one. Key difference from OCaml: 1. **Applicative vs monadic**: `Validated` is applicative (both sides computed); `Result` is monadic (short

Tutorial

The Problem

User registration forms, configuration validation, and batch processing all share a need: show all errors at once, not just the first one. When a form has 10 invalid fields, showing only the first error forces the user to submit nine more times. The Validated type addresses this with applicative composition: validate all fields independently, then combine results — accumulating every error if multiple validations fail simultaneously.

🎯 Learning Outcomes

  • • Understand the difference between monadic (Result) and applicative (Validated) error handling
  • • Implement Validated<T, E> with valid(), invalid(), map(), and combine() operations
  • • Use Validated to validate multiple independent fields simultaneously
  • • Recognize when accumulation (show all errors) vs short-circuit (stop at first) is the right strategy
  • Code Example

    #![allow(clippy::all)]
    //! # Accumulating Multiple Errors (Validated)
    //!
    //! Validated accumulates ALL errors, unlike Result which stops at first.
    
    /// Validated type for error accumulation
    #[derive(Debug, PartialEq)]
    pub enum Validated<T, E> {
        Valid(T),
        Invalid(Vec<E>),
    }
    
    impl<T, E> Validated<T, E> {
        pub fn valid(v: T) -> Self {
            Validated::Valid(v)
        }
        pub fn invalid(e: E) -> Self {
            Validated::Invalid(vec![e])
        }
    
        pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Validated<U, E> {
            match self {
                Validated::Valid(v) => Validated::Valid(f(v)),
                Validated::Invalid(es) => Validated::Invalid(es),
            }
        }
    }
    
    pub fn combine<A, B, E>(a: Validated<A, E>, b: Validated<B, E>) -> Validated<(A, B), E> {
        match (a, b) {
            (Validated::Valid(a), Validated::Valid(b)) => Validated::Valid((a, b)),
            (Validated::Invalid(mut e1), Validated::Invalid(e2)) => {
                e1.extend(e2);
                Validated::Invalid(e1)
            }
            (Validated::Invalid(e), _) | (_, Validated::Invalid(e)) => Validated::Invalid(e),
        }
    }
    
    pub fn validate_name(name: &str) -> Validated<String, String> {
        if name.is_empty() {
            return Validated::invalid("name cannot be empty".into());
        }
        Validated::valid(name.to_string())
    }
    
    pub fn validate_email(email: &str) -> Validated<String, String> {
        if !email.contains('@') {
            return Validated::invalid(format!("invalid email: {}", email));
        }
        Validated::valid(email.to_string())
    }
    
    pub fn validate_age(age_str: &str) -> Validated<u8, String> {
        match age_str.parse::<i32>() {
            Ok(n) if (0..=150).contains(&n) => Validated::valid(n as u8),
            Ok(n) => Validated::invalid(format!("age {} out of range", n)),
            Err(_) => Validated::invalid(format!("'{}' is not a number", age_str)),
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let name = validate_name("Alice");
            let email = validate_email("alice@example.com");
            let r = combine(name, email);
            assert!(matches!(r, Validated::Valid(_)));
        }
    
        #[test]
        fn test_accumulate_two_errors() {
            let name = validate_name("");
            let email = validate_email("bad");
            let r = combine(name, email);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Invalid");
            }
        }
    
        #[test]
        fn test_accumulate_three() {
            let name = validate_name("");
            let email = validate_email("bad");
            let age = validate_age("999");
            let r = combine(combine(name, email), age);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 3);
            } else {
                panic!("Expected Invalid");
            }
        }
    }

    Key Differences

  • Applicative vs monadic: Validated is applicative (both sides computed); Result is monadic (short-circuits on Err).
  • Semantic choice: Short-circuit when errors are dependent (step 2 requires step 1); accumulate when errors are independent (all form fields).
  • Production use: The Rust garde and validator crates use accumulation for struct validation — all field errors are collected and returned together.
  • Conversion: Validated<T, E> can be converted to Result<T, Vec<E>> by taking the first error or collecting all errors into a summary.
  • OCaml Approach

    OCaml's Ppx_let and applicative functors support this pattern. Lwt.both and similar functions provide concurrent validation with error accumulation:

    type ('a, 'e) validated = Valid of 'a | Invalid of 'e list
    
    let and_validate v1 v2 = match (v1, v2) with
      | (Valid x, Valid y) -> Valid (x, y)
      | (Invalid es1, Invalid es2) -> Invalid (es1 @ es2)
      | (Invalid es, _) | (_, Invalid es) -> Invalid es
    

    Full Source

    #![allow(clippy::all)]
    //! # Accumulating Multiple Errors (Validated)
    //!
    //! Validated accumulates ALL errors, unlike Result which stops at first.
    
    /// Validated type for error accumulation
    #[derive(Debug, PartialEq)]
    pub enum Validated<T, E> {
        Valid(T),
        Invalid(Vec<E>),
    }
    
    impl<T, E> Validated<T, E> {
        pub fn valid(v: T) -> Self {
            Validated::Valid(v)
        }
        pub fn invalid(e: E) -> Self {
            Validated::Invalid(vec![e])
        }
    
        pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Validated<U, E> {
            match self {
                Validated::Valid(v) => Validated::Valid(f(v)),
                Validated::Invalid(es) => Validated::Invalid(es),
            }
        }
    }
    
    pub fn combine<A, B, E>(a: Validated<A, E>, b: Validated<B, E>) -> Validated<(A, B), E> {
        match (a, b) {
            (Validated::Valid(a), Validated::Valid(b)) => Validated::Valid((a, b)),
            (Validated::Invalid(mut e1), Validated::Invalid(e2)) => {
                e1.extend(e2);
                Validated::Invalid(e1)
            }
            (Validated::Invalid(e), _) | (_, Validated::Invalid(e)) => Validated::Invalid(e),
        }
    }
    
    pub fn validate_name(name: &str) -> Validated<String, String> {
        if name.is_empty() {
            return Validated::invalid("name cannot be empty".into());
        }
        Validated::valid(name.to_string())
    }
    
    pub fn validate_email(email: &str) -> Validated<String, String> {
        if !email.contains('@') {
            return Validated::invalid(format!("invalid email: {}", email));
        }
        Validated::valid(email.to_string())
    }
    
    pub fn validate_age(age_str: &str) -> Validated<u8, String> {
        match age_str.parse::<i32>() {
            Ok(n) if (0..=150).contains(&n) => Validated::valid(n as u8),
            Ok(n) => Validated::invalid(format!("age {} out of range", n)),
            Err(_) => Validated::invalid(format!("'{}' is not a number", age_str)),
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let name = validate_name("Alice");
            let email = validate_email("alice@example.com");
            let r = combine(name, email);
            assert!(matches!(r, Validated::Valid(_)));
        }
    
        #[test]
        fn test_accumulate_two_errors() {
            let name = validate_name("");
            let email = validate_email("bad");
            let r = combine(name, email);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Invalid");
            }
        }
    
        #[test]
        fn test_accumulate_three() {
            let name = validate_name("");
            let email = validate_email("bad");
            let age = validate_age("999");
            let r = combine(combine(name, email), age);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 3);
            } else {
                panic!("Expected Invalid");
            }
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_all_valid() {
            let name = validate_name("Alice");
            let email = validate_email("alice@example.com");
            let r = combine(name, email);
            assert!(matches!(r, Validated::Valid(_)));
        }
    
        #[test]
        fn test_accumulate_two_errors() {
            let name = validate_name("");
            let email = validate_email("bad");
            let r = combine(name, email);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Invalid");
            }
        }
    
        #[test]
        fn test_accumulate_three() {
            let name = validate_name("");
            let email = validate_email("bad");
            let age = validate_age("999");
            let r = combine(combine(name, email), age);
            if let Validated::Invalid(errs) = r {
                assert_eq!(errs.len(), 3);
            } else {
                panic!("Expected Invalid");
            }
        }
    }

    Deep Comparison

    validated-accumulation

    See README.md for details.

    Exercises

  • Extend the user validator with a third field (email must contain @) and verify that invalid name + invalid email reports both errors.
  • Implement a traverse function: fn traverse<T, U, E>(items: Vec<T>, f: impl Fn(T) -> Validated<U, E>) -> Validated<Vec<U>, E> that validates all items and accumulates all errors.
  • Compare the output of Validated vs Result on the same validation logic — show that Result stops at first failure while Validated collects all.
  • Open Source Repos