ExamplesBy LevelBy TopicLearning Paths
852 Intermediate

Applicative Functor Basics

Functional Programming

Tutorial

The Problem

Functors apply a plain function to a wrapped value: map(f, Just(x)) = Just(f(x)). But what if the function itself is wrapped? Applicative functors add apply(Just(f), Just(x)) = Just(f(x)) — applying a wrapped function to a wrapped value. This enables combining multiple independent computations: parse two fields from a form, validate them independently, and combine results only if both succeed. Applicatives are strictly more powerful than functors but less powerful than monads (monads allow the second computation to depend on the first). In practice: form validation (Validated), parallel effects, command-line parsing (clap's applicative API), and parser combinators all use applicative structure.

🎯 Learning Outcomes

  • • Understand pure(x) = Just(x): lifting a plain value into the applicative context
  • • Understand apply(mf, mx): apply a wrapped function to a wrapped value
  • • Verify applicative laws: identity, composition, homomorphism, interchange
  • • Recognize the difference from monads: applicatives combine independent effects; monads sequence dependent effects
  • • Apply to: combining two Option values, two Result values without early-exit chaining
  • Code Example

    impl<F> Maybe<F> {
        fn apply<A, B>(self, ma: Maybe<A>) -> Maybe<B>
        where F: FnOnce(A) -> B {
            match (self, ma) {
                (Maybe::Just(f), Maybe::Just(a)) => Maybe::Just(f(a)),
                _ => Maybe::Nothing,
            }
        }
    }

    Key Differences

    AspectRustOCaml
    pureMaybe::pure(x)pure x or Some x
    apply.apply(mf) method<*> infix operator
    CurryingManual (closures returning closures)Automatic
    Multi-arg combinezip or nested applyf <*> mx <*> my
    HKT limitationCannot express generic ApplicativeModule functor can
    Independent effectsYes (no dependency between args)Same

    OCaml Approach

    OCaml's applicative is expressed via a module signature: module type APPLICATIVE = sig include FUNCTOR; val pure : 'a -> 'a t; val (<*>) : ('a -> 'b) t -> 'a t -> 'b t end. The <*> operator applies wrapped functions. let ( <*> ) mf mx = match mf, mx with Some f, Some x -> Some (f x) | _ -> None. Currying in OCaml makes multi-argument applicative clean: Some (+) <*> Some 3 <*> Some 4 = Some 7. The Applicative interface underlies OCaml's Angstrom parser combinator library.

    Full Source

    #![allow(clippy::all)]
    // Example 053: Applicative Functor Basics
    // Applicative: apply a wrapped function to a wrapped value
    
    #[derive(Debug, PartialEq, Clone)]
    enum Maybe<T> {
        Nothing,
        Just(T),
    }
    
    impl<T> Maybe<T> {
        fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Maybe<U> {
            match self {
                Maybe::Nothing => Maybe::Nothing,
                Maybe::Just(x) => Maybe::Just(f(x)),
            }
        }
    
        fn pure(x: T) -> Maybe<T> {
            Maybe::Just(x)
        }
    }
    
    // Approach 1: Apply — apply a wrapped function to a wrapped value
    impl<F> Maybe<F> {
        fn apply<A, B>(self, ma: Maybe<A>) -> Maybe<B>
        where
            F: FnOnce(A) -> B,
        {
            match (self, ma) {
                (Maybe::Just(f), Maybe::Just(a)) => Maybe::Just(f(a)),
                _ => Maybe::Nothing,
            }
        }
    }
    
    // Approach 2: lift2 / lift3 as free functions
    fn lift2<A, B, C, F>(f: F, a: Maybe<A>, b: Maybe<B>) -> Maybe<C>
    where
        F: FnOnce(A) -> Box<dyn FnOnce(B) -> C>,
    {
        match (a, b) {
            (Maybe::Just(a), Maybe::Just(b)) => Maybe::Just(f(a)(b)),
            _ => Maybe::Nothing,
        }
    }
    
    // Simpler lift2 without currying
    fn lift2_simple<A, B, C, F: FnOnce(A, B) -> C>(f: F, a: Maybe<A>, b: Maybe<B>) -> Maybe<C> {
        match (a, b) {
            (Maybe::Just(a), Maybe::Just(b)) => Maybe::Just(f(a, b)),
            _ => Maybe::Nothing,
        }
    }
    
    fn lift3_simple<A, B, C, D, F: FnOnce(A, B, C) -> D>(
        f: F,
        a: Maybe<A>,
        b: Maybe<B>,
        c: Maybe<C>,
    ) -> Maybe<D> {
        match (a, b, c) {
            (Maybe::Just(a), Maybe::Just(b), Maybe::Just(c)) => Maybe::Just(f(a, b, c)),
            _ => Maybe::Nothing,
        }
    }
    
    // Approach 3: Using Option's built-in zip (Rust's applicative)
    fn option_applicative_example() -> Option<(i32, i32)> {
        let a = "42".parse::<i32>().ok();
        let b = "7".parse::<i32>().ok();
        a.zip(b) // Option's built-in applicative-like combinator
    }
    
    fn parse_int(s: &str) -> Maybe<i32> {
        match s.parse::<i32>() {
            Ok(n) => Maybe::Just(n),
            Err(_) => Maybe::Nothing,
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_both_just() {
            let f = Maybe::Just(|x: i32| x * 2);
            assert_eq!(f.apply(Maybe::Just(5)), Maybe::Just(10));
        }
    
        #[test]
        fn test_apply_nothing_function() {
            let f: Maybe<fn(i32) -> i32> = Maybe::Nothing;
            assert_eq!(f.apply(Maybe::Just(5)), Maybe::Nothing);
        }
    
        #[test]
        fn test_apply_nothing_value() {
            let f = Maybe::Just(|x: i32| x * 2);
            assert_eq!(f.apply(Maybe::Nothing), Maybe::Nothing);
        }
    
        #[test]
        fn test_lift2_both_just() {
            assert_eq!(
                lift2_simple(|a: i32, b: i32| a + b, Maybe::Just(10), Maybe::Just(20)),
                Maybe::Just(30)
            );
        }
    
        #[test]
        fn test_lift2_one_nothing() {
            assert_eq!(
                lift2_simple(|a: i32, b: i32| a + b, Maybe::Nothing, Maybe::Just(20)),
                Maybe::Nothing
            );
        }
    
        #[test]
        fn test_lift3() {
            let result = lift3_simple(
                |a: &str, b: &str, c: &str| format!("{}{}{}", a, b, c),
                Maybe::Just("x"),
                Maybe::Just("y"),
                Maybe::Just("z"),
            );
            assert_eq!(result, Maybe::Just("xyz".to_string()));
        }
    
        #[test]
        fn test_option_zip() {
            assert_eq!(option_applicative_example(), Some((42, 7)));
        }
    
        #[test]
        fn test_parse_and_combine() {
            let result = lift2_simple(|a: i32, b: i32| a + b, parse_int("42"), parse_int("8"));
            assert_eq!(result, Maybe::Just(50));
            let result2 = lift2_simple(|a: i32, b: i32| a + b, parse_int("bad"), parse_int("8"));
            assert_eq!(result2, Maybe::Nothing);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_both_just() {
            let f = Maybe::Just(|x: i32| x * 2);
            assert_eq!(f.apply(Maybe::Just(5)), Maybe::Just(10));
        }
    
        #[test]
        fn test_apply_nothing_function() {
            let f: Maybe<fn(i32) -> i32> = Maybe::Nothing;
            assert_eq!(f.apply(Maybe::Just(5)), Maybe::Nothing);
        }
    
        #[test]
        fn test_apply_nothing_value() {
            let f = Maybe::Just(|x: i32| x * 2);
            assert_eq!(f.apply(Maybe::Nothing), Maybe::Nothing);
        }
    
        #[test]
        fn test_lift2_both_just() {
            assert_eq!(
                lift2_simple(|a: i32, b: i32| a + b, Maybe::Just(10), Maybe::Just(20)),
                Maybe::Just(30)
            );
        }
    
        #[test]
        fn test_lift2_one_nothing() {
            assert_eq!(
                lift2_simple(|a: i32, b: i32| a + b, Maybe::Nothing, Maybe::Just(20)),
                Maybe::Nothing
            );
        }
    
        #[test]
        fn test_lift3() {
            let result = lift3_simple(
                |a: &str, b: &str, c: &str| format!("{}{}{}", a, b, c),
                Maybe::Just("x"),
                Maybe::Just("y"),
                Maybe::Just("z"),
            );
            assert_eq!(result, Maybe::Just("xyz".to_string()));
        }
    
        #[test]
        fn test_option_zip() {
            assert_eq!(option_applicative_example(), Some((42, 7)));
        }
    
        #[test]
        fn test_parse_and_combine() {
            let result = lift2_simple(|a: i32, b: i32| a + b, parse_int("42"), parse_int("8"));
            assert_eq!(result, Maybe::Just(50));
            let result2 = lift2_simple(|a: i32, b: i32| a + b, parse_int("bad"), parse_int("8"));
            assert_eq!(result2, Maybe::Nothing);
        }
    }

    Deep Comparison

    Comparison: Applicative Functor Basics

    Apply Operation

    OCaml:

    let apply mf mx = match mf with
      | Nothing -> Nothing
      | Just f -> map f mx
    
    let ( <*> ) = apply
    
    (* Usage: pure add <*> Just 3 <*> Just 4 = Just 7 *)
    

    Rust:

    impl<F> Maybe<F> {
        fn apply<A, B>(self, ma: Maybe<A>) -> Maybe<B>
        where F: FnOnce(A) -> B {
            match (self, ma) {
                (Maybe::Just(f), Maybe::Just(a)) => Maybe::Just(f(a)),
                _ => Maybe::Nothing,
            }
        }
    }
    

    Lifting Multi-Argument Functions

    OCaml:

    (* Currying makes this elegant *)
    let lift2 f a b = (pure f) <*> a <*> b
    let result = lift2 (+) (Just 10) (Just 20)  (* Just 30 *)
    

    Rust:

    // No currying — take multi-arg closure directly
    fn lift2_simple<A, B, C, F: FnOnce(A, B) -> C>(
        f: F, a: Maybe<A>, b: Maybe<B>,
    ) -> Maybe<C> {
        match (a, b) {
            (Maybe::Just(a), Maybe::Just(b)) => Maybe::Just(f(a, b)),
            _ => Maybe::Nothing,
        }
    }
    
    let result = lift2_simple(|a, b| a + b, Maybe::Just(10), Maybe::Just(20));
    

    Built-in Applicative in Rust

    Rust (Option::zip):

    let a = Some(3);
    let b = Some(4);
    let result = a.zip(b).map(|(a, b)| a + b); // Some(7)
    

    Exercises

  • Implement map2(f, mx, my) for Maybe using apply and pure: combine two independent Maybes with a binary function.
  • Verify the applicative identity law: pure(id).apply(mx) == mx.
  • Verify the homomorphism law: pure(f).apply(pure(x)) == pure(f(x)).
  • Implement applicative for Result<T, E>: both values must be Ok; if either is Err, return the first Err.
  • Compare applicative and monadic composition: show that applicative(f, option1, option2) cannot express "parse field2 differently based on field1's value."
  • Open Source Repos