ExamplesBy LevelBy TopicLearning Paths
313 Intermediate

313: The Try Trait — What ? Actually Does

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "313: The Try Trait — What ? Actually Does" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The `?` operator desugars to a call to the `Try` trait (unstable) or the earlier `From` + early-return pattern. Key difference from OCaml: 1. **Monad vs applicative**: Short

Tutorial

The Problem

The ? operator desugars to a call to the Try trait (unstable) or the earlier From + early-return pattern. This example demonstrates the concept using a Validated<T, E> type that accumulates multiple errors instead of short-circuiting — illustrating that the ? behavior is customizable. Understanding what ? actually does enables implementing custom types that participate in Rust's error-handling ergonomics.

🎯 Learning Outcomes

  • • Understand ? as desugaring to: extract value or convert error and return early
  • • Implement a Validated type that accumulates errors instead of short-circuiting
  • • Recognize that ? semantics are defined by the return type, not the operator itself
  • • See how Result and Option implement the early-return contract that ? relies on
  • Code Example

    #![allow(clippy::all)]
    //! # The Try Trait and Custom ? Behavior
    //!
    //! Validated type that accumulates errors instead of short-circuiting.
    
    /// Validated type - accumulates errors applicatively
    #[derive(Debug, PartialEq)]
    pub enum Validated<T, E> {
        Ok(T),
        Err(Vec<E>),
    }
    
    impl<T, E> Validated<T, E> {
        pub fn ok(v: T) -> Self {
            Validated::Ok(v)
        }
        pub fn err(e: E) -> Self {
            Validated::Err(vec![e])
        }
    
        pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Validated<U, E> {
            match self {
                Validated::Ok(v) => Validated::Ok(f(v)),
                Validated::Err(es) => Validated::Err(es),
            }
        }
    
        pub fn and<U>(self, other: Validated<U, E>) -> Validated<(T, U), E> {
            match (self, other) {
                (Validated::Ok(a), Validated::Ok(b)) => Validated::Ok((a, b)),
                (Validated::Err(mut e1), Validated::Err(e2)) => {
                    e1.extend(e2);
                    Validated::Err(e1)
                }
                (Validated::Err(e), _) | (_, Validated::Err(e)) => Validated::Err(e),
            }
        }
    
        pub fn is_ok(&self) -> bool {
            matches!(self, Validated::Ok(_))
        }
    }
    
    pub fn validate_age(age: i32) -> Validated<i32, String> {
        if age >= 0 && age <= 150 {
            Validated::ok(age)
        } else {
            Validated::err(format!("age {} is out of range", age))
        }
    }
    
    pub fn validate_name(name: &str) -> Validated<String, String> {
        if name.len() >= 2 && name.len() <= 50 {
            Validated::ok(name.to_string())
        } else {
            Validated::err(format!("name '{}' is invalid", name))
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_validated_ok() {
            assert_eq!(validate_age(25), Validated::Ok(25));
        }
    
        #[test]
        fn test_validated_err() {
            assert!(matches!(validate_age(999), Validated::Err(_)));
        }
    
        #[test]
        fn test_accumulate_errors() {
            let r = validate_age(999).and(validate_name("X"));
            if let Validated::Err(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Err");
            }
        }
    
        #[test]
        fn test_and_both_ok() {
            let r = validate_age(25).and(validate_name("Alice"));
            assert!(r.is_ok());
        }
    
        #[test]
        fn test_map() {
            let r = validate_age(25).map(|a| a * 2);
            assert_eq!(r, Validated::Ok(50));
        }
    }

    Key Differences

  • Monad vs applicative: Short-circuit (Result, Option) is monadic; error accumulation (Validated) is applicative — fundamentally different composition strategies.
  • **? limitation**: Rust's ? is monadic (short-circuit only); accumulation requires explicit and() or similar applicative operations.
  • Form validation: Accumulation is the right strategy for form validation — show all errors at once, not one at a time.
  • Cats/Haskell: Haskell's Validation type in validation crate / Data.Validation directly mirrors this; PureScript, Elm, and other FP languages have similar types.
  • OCaml Approach

    OCaml's let* desugars to bind — the behavior is determined by the monad, not the syntax. A Validated monad in OCaml accumulates errors in its bind (applicative) form:

    (* Applicative validation: both branches evaluated, errors accumulated *)
    let validate_both v1 v2 = match (v1, v2) with
      | (Valid x, Valid y) -> Valid (x, y)
      | (Invalid e1, Invalid e2) -> Invalid (e1 @ e2)
      | (Invalid e, _) | (_, Invalid e) -> Invalid e
    

    Full Source

    #![allow(clippy::all)]
    //! # The Try Trait and Custom ? Behavior
    //!
    //! Validated type that accumulates errors instead of short-circuiting.
    
    /// Validated type - accumulates errors applicatively
    #[derive(Debug, PartialEq)]
    pub enum Validated<T, E> {
        Ok(T),
        Err(Vec<E>),
    }
    
    impl<T, E> Validated<T, E> {
        pub fn ok(v: T) -> Self {
            Validated::Ok(v)
        }
        pub fn err(e: E) -> Self {
            Validated::Err(vec![e])
        }
    
        pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Validated<U, E> {
            match self {
                Validated::Ok(v) => Validated::Ok(f(v)),
                Validated::Err(es) => Validated::Err(es),
            }
        }
    
        pub fn and<U>(self, other: Validated<U, E>) -> Validated<(T, U), E> {
            match (self, other) {
                (Validated::Ok(a), Validated::Ok(b)) => Validated::Ok((a, b)),
                (Validated::Err(mut e1), Validated::Err(e2)) => {
                    e1.extend(e2);
                    Validated::Err(e1)
                }
                (Validated::Err(e), _) | (_, Validated::Err(e)) => Validated::Err(e),
            }
        }
    
        pub fn is_ok(&self) -> bool {
            matches!(self, Validated::Ok(_))
        }
    }
    
    pub fn validate_age(age: i32) -> Validated<i32, String> {
        if age >= 0 && age <= 150 {
            Validated::ok(age)
        } else {
            Validated::err(format!("age {} is out of range", age))
        }
    }
    
    pub fn validate_name(name: &str) -> Validated<String, String> {
        if name.len() >= 2 && name.len() <= 50 {
            Validated::ok(name.to_string())
        } else {
            Validated::err(format!("name '{}' is invalid", name))
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_validated_ok() {
            assert_eq!(validate_age(25), Validated::Ok(25));
        }
    
        #[test]
        fn test_validated_err() {
            assert!(matches!(validate_age(999), Validated::Err(_)));
        }
    
        #[test]
        fn test_accumulate_errors() {
            let r = validate_age(999).and(validate_name("X"));
            if let Validated::Err(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Err");
            }
        }
    
        #[test]
        fn test_and_both_ok() {
            let r = validate_age(25).and(validate_name("Alice"));
            assert!(r.is_ok());
        }
    
        #[test]
        fn test_map() {
            let r = validate_age(25).map(|a| a * 2);
            assert_eq!(r, Validated::Ok(50));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_validated_ok() {
            assert_eq!(validate_age(25), Validated::Ok(25));
        }
    
        #[test]
        fn test_validated_err() {
            assert!(matches!(validate_age(999), Validated::Err(_)));
        }
    
        #[test]
        fn test_accumulate_errors() {
            let r = validate_age(999).and(validate_name("X"));
            if let Validated::Err(errs) = r {
                assert_eq!(errs.len(), 2);
            } else {
                panic!("Expected Err");
            }
        }
    
        #[test]
        fn test_and_both_ok() {
            let r = validate_age(25).and(validate_name("Alice"));
            assert!(r.is_ok());
        }
    
        #[test]
        fn test_map() {
            let r = validate_age(25).map(|a| a * 2);
            assert_eq!(r, Validated::Ok(50));
        }
    }

    Deep Comparison

    try-trait

    See README.md for details.

    Exercises

  • Implement a form validator using Validated<T, String> that validates a name (non-empty) and age (18-100) simultaneously, returning all errors if both fail.
  • Add an and_then method to Validated that behaves identically to Result::and_then (short-circuits on Err) — show when to use each.
  • Convert between Validated<T, Vec<E>> and Result<T, Vec<E>> — what information is preserved or lost in each direction?
  • Open Source Repos