314: Validated — Accumulating All Errors
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
Result) and applicative (Validated) error handlingValidated<T, E> with valid(), invalid(), map(), and combine() operationsValidated to validate multiple independent fields simultaneouslyCode 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
Validated is applicative (both sides computed); Result is monadic (short-circuits on Err).garde and validator crates use accumulation for struct validation — all field errors are collected and returned together.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");
}
}
}#[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
@) and verify that invalid name + invalid email reports both errors.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.Validated vs Result on the same validation logic — show that Result stops at first failure while Validated collects all.