ExamplesBy LevelBy TopicLearning Paths
862 Expert

Monad Transformers

Functional Programming

Tutorial

The Problem

Real programs need multiple effects simultaneously: a computation might be fallible (Option/Result), AND need to read configuration (Reader), AND accumulate a log (Writer). Composing two monads directly doesn't work — Option<Reader<Config, A>> and Reader<Config, Option<A>> require different bind implementations. Monad transformers solve this by stacking effects: OptionT<E, A> = Result<Option<A>, E> adds optionality to Result. This enables writing code that handles both "not found" (None) and "failed" (Err) without explicit nesting. Used in: web frameworks (request handler: Result for errors, Option for optional fields, Reader for context), compiler pipelines, and effectful DSLs.

🎯 Learning Outcomes

  • • Understand OptionT<A, E> = Result<Option<A>, E> as Option inside Result
  • • Implement lift_option and lift_result to promote values into the transformer
  • • Implement bind that threads both effects: Err short-circuits, Ok(None) propagates absence
  • • Recognize the tradeoff: transformers are complex; often explicit nesting is clearer
  • • Apply to: handlers returning Result<Option<User>, DbError> for "not found" vs. "db failed"
  • Code Example

    fn bind<A, B, E>(m: Result<Option<A>, E>, f: impl FnOnce(A) -> Result<Option<B>, E>) -> Result<Option<B>, E> {
        match m {
            Err(e) => Err(e),
            Ok(None) => Ok(None),
            Ok(Some(a)) => f(a),
        }
    }

    Key Differences

    AspectRustOCaml
    Typetype OptionT<A,E> = Result<Option<A>,E>Algebraic type or module type
    Generic transformerNot expressible (no HKT)Module functor OptionT(M: MONAD)
    BindFree function bind_optMethod or functor-generated
    lift_resultr.map(Some)Result.map Option.some r
    Three-way matchmatch ma { Err, Ok(None), Ok(Some) }Same
    UsageType alias + functionsModule with t, bind, etc.

    OCaml Approach

    OCaml's OptionT module: type ('a, 'e) t = ('a option, 'e) result. The bind: let bind ma f = match ma with Error e -> Error e | Ok None -> Ok None | Ok (Some a) -> f a. lift_result r = Result.map Option.some r. lift_option o = Ok o. OCaml module functors parameterize over the base monad: module OptionT(M: MONAD) = struct ... generates the transformer for any monad M. This generality requires HKTs, which OCaml achieves through module functors.

    Full Source

    #![allow(clippy::all)]
    // Example 063: Monad Transformers
    // Stacking monads: Option inside Result
    
    // OptionT<E, A> = Result<Option<A>, E>
    type OptionT<A, E> = Result<Option<A>, E>;
    
    // Approach 1: Helper functions for OptionT
    mod option_t {
        pub fn pure<A, E>(a: A) -> Result<Option<A>, E> {
            Ok(Some(a))
        }
    
        pub fn none<A, E>() -> Result<Option<A>, E> {
            Ok(None)
        }
    
        pub fn bind<A, B, E>(
            m: Result<Option<A>, E>,
            f: impl FnOnce(A) -> Result<Option<B>, E>,
        ) -> Result<Option<B>, E> {
            match m {
                Err(e) => Err(e),
                Ok(None) => Ok(None),
                Ok(Some(a)) => f(a),
            }
        }
    
        pub fn map<A, B, E>(m: Result<Option<A>, E>, f: impl FnOnce(A) -> B) -> Result<Option<B>, E> {
            match m {
                Err(e) => Err(e),
                Ok(None) => Ok(None),
                Ok(Some(a)) => Ok(Some(f(a))),
            }
        }
    
        pub fn lift_result<A, E>(r: Result<A, E>) -> Result<Option<A>, E> {
            r.map(Some)
        }
    
        pub fn lift_option<A, E>(o: Option<A>) -> Result<Option<A>, E> {
            Ok(o)
        }
    }
    
    // Approach 2: Database operations
    fn find_user(id: i32) -> OptionT<String, String> {
        if id > 0 {
            Ok(Some(format!("User_{}", id)))
        } else if id == 0 {
            Ok(None)
        } else {
            Err("Invalid ID".to_string())
        }
    }
    
    fn find_email(name: &str) -> OptionT<String, String> {
        match name {
            "User_1" => Ok(Some("user1@example.com".to_string())),
            "User_2" => Ok(None),
            _ => Err("DB connection failed".to_string()),
        }
    }
    
    fn get_user_email(id: i32) -> OptionT<String, String> {
        option_t::bind(find_user(id), |name| find_email(&name))
    }
    
    // Approach 3: Using ? with nested unwrapping (idiomatic Rust)
    fn get_user_email_idiomatic(id: i32) -> Result<Option<String>, String> {
        let user = match find_user(id)? {
            Some(u) => u,
            None => return Ok(None),
        };
        find_email(&user)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_found_with_email() {
            assert_eq!(get_user_email(1), Ok(Some("user1@example.com".to_string())));
        }
    
        #[test]
        fn test_user_not_found() {
            assert_eq!(get_user_email(0), Ok(None));
        }
    
        #[test]
        fn test_invalid_id() {
            assert_eq!(get_user_email(-1), Err("Invalid ID".to_string()));
        }
    
        #[test]
        fn test_user_no_email() {
            assert_eq!(get_user_email(2), Ok(None));
        }
    
        #[test]
        fn test_map() {
            let upper = option_t::map(get_user_email(1), |s| s.to_uppercase());
            assert_eq!(upper, Ok(Some("USER1@EXAMPLE.COM".to_string())));
        }
    
        #[test]
        fn test_lift_result() {
            assert_eq!(option_t::lift_result::<_, String>(Ok(42)), Ok(Some(42)));
            assert_eq!(
                option_t::lift_result::<i32, _>(Err("e".to_string())),
                Err("e".to_string())
            );
        }
    
        #[test]
        fn test_lift_option() {
            assert_eq!(option_t::lift_option::<_, String>(Some(42)), Ok(Some(42)));
            assert_eq!(option_t::lift_option::<i32, String>(None), Ok(None));
        }
    
        #[test]
        fn test_idiomatic_same_results() {
            for id in [-1, 0, 1, 2] {
                assert_eq!(get_user_email(id), get_user_email_idiomatic(id));
            }
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_found_with_email() {
            assert_eq!(get_user_email(1), Ok(Some("user1@example.com".to_string())));
        }
    
        #[test]
        fn test_user_not_found() {
            assert_eq!(get_user_email(0), Ok(None));
        }
    
        #[test]
        fn test_invalid_id() {
            assert_eq!(get_user_email(-1), Err("Invalid ID".to_string()));
        }
    
        #[test]
        fn test_user_no_email() {
            assert_eq!(get_user_email(2), Ok(None));
        }
    
        #[test]
        fn test_map() {
            let upper = option_t::map(get_user_email(1), |s| s.to_uppercase());
            assert_eq!(upper, Ok(Some("USER1@EXAMPLE.COM".to_string())));
        }
    
        #[test]
        fn test_lift_result() {
            assert_eq!(option_t::lift_result::<_, String>(Ok(42)), Ok(Some(42)));
            assert_eq!(
                option_t::lift_result::<i32, _>(Err("e".to_string())),
                Err("e".to_string())
            );
        }
    
        #[test]
        fn test_lift_option() {
            assert_eq!(option_t::lift_option::<_, String>(Some(42)), Ok(Some(42)));
            assert_eq!(option_t::lift_option::<i32, String>(None), Ok(None));
        }
    
        #[test]
        fn test_idiomatic_same_results() {
            for id in [-1, 0, 1, 2] {
                assert_eq!(get_user_email(id), get_user_email_idiomatic(id));
            }
        }
    }

    Deep Comparison

    Comparison: Monad Transformers

    OptionT Bind

    OCaml:

    let bind m f = match m with
      | Error e -> Error e
      | Ok None -> Ok None
      | Ok (Some a) -> f a
    

    Rust:

    fn bind<A, B, E>(m: Result<Option<A>, E>, f: impl FnOnce(A) -> Result<Option<B>, E>) -> Result<Option<B>, E> {
        match m {
            Err(e) => Err(e),
            Ok(None) => Ok(None),
            Ok(Some(a)) => f(a),
        }
    }
    

    Chained OptionT

    OCaml:

    find_user id >>= fun name -> find_email name
    

    Rust (transformer):

    option_t::bind(find_user(id), |name| find_email(&name))
    

    Rust (idiomatic with ? and early return):

    fn get_user_email(id: i32) -> Result<Option<String>, String> {
        let user = match find_user(id)? {
            Some(u) => u,
            None => return Ok(None),   // early exit for "not found"
        };
        find_email(&user)
    }
    

    Lifting

    OCaml:

    let lift_result r = Result.map (fun x -> Some x) r
    let lift_option o = Ok o
    

    Rust:

    fn lift_result<A, E>(r: Result<A, E>) -> Result<Option<A>, E> { r.map(Some) }
    fn lift_option<A, E>(o: Option<A>) -> Result<Option<A>, E> { Ok(o) }
    

    Exercises

  • Implement ResultT<A, E, W> = Writer<Result<A, E>, W> — a computation that can fail AND accumulates a log.
  • Use OptionT<A, E> to implement a database lookup that distinguishes "not found" from "database error."
  • Implement sequence_opt(ops: Vec<OptionT<A, E>>) -> OptionT<Vec<A>, E> that collects all results if none are absent or erroneous.
  • Compare explicit nesting (Result<Option<A>, E> with manual match) vs. OptionT's bind_opt for a multi-step pipeline.
  • Add a ReaderT<R, A, E> = Reader<R, Result<A, E>> and implement its bind operation.
  • Open Source Repos