๐Ÿฆ€ Functional Rust

053: Applicative Functor Basics

Difficulty: 2 Level: Intermediate Apply a wrapped function to a wrapped value โ€” the missing link between `map` and `and_then`.

The Problem This Solves

You know `Option::map`. You give it a plain function and it applies it inside the `Option`. Simple. But what happens when the function itself is wrapped in an `Option`? Maybe it came from a lookup table, maybe it's user-configured, maybe it could be absent. `map` can't handle that โ€” it only takes plain functions. You also know `and_then` (Rust's flatMap). That chains steps where each step depends on the result of the previous one. But sometimes your values are independent โ€” you have two separate `Option<i32>` values and want to add them. You don't need to chain them; you need to combine them in parallel. The gap: `map` applies one plain function, `and_then` chains dependent steps, but there's no clean built-in for "combine two or three independent wrapped values using a plain function." You end up writing the same nested `match` pattern over and over: `match (a, b) { (Some(x), Some(y)) => Some(f(x, y)), _ => None }`. Every time. For every function. For every pair of values. Applicative fills this gap. It gives you `apply` (for wrapped functions) and `lift2`/`lift3` (for combining 2 or 3 independent wrapped values). This exists to solve exactly that pain.

The Intuition

Think of `Option` as a box. `map` takes a regular function and applies it to whatever is in the box. But what if the function is also in a box? Applicative says: if you have a box containing a function, and a box containing a value, you can produce a box containing the result โ€” or `Nothing` if either box is empty. `lift2` is even simpler to understand: "give me a regular two-argument function and two wrapped values, and I'll apply the function if both are present." It's like `zip` + `map` in one shot.
// You have these:
let maybe_name: Maybe<String> = Maybe::Just("Alice".to_string());
let maybe_age:  Maybe<i32>    = Maybe::Just(30);

// You want this โ€” but both inputs are optional:
// User { name: "Alice", age: 30 }

// Without applicative (nested match):
let user = match (maybe_name, maybe_age) {
 (Maybe::Just(name), Maybe::Just(age)) => Maybe::Just(User { name, age }),
 _ => Maybe::Nothing,
};

// With lift2:
let user = lift2_simple(
 |name, age| User { name, age },
 maybe_name,
 maybe_age,
);
// Same result, half the noise
Jargon decoded:

How It Works in Rust

#[derive(Debug, PartialEq, Clone)]
enum Maybe<T> {
 Nothing,
 Just(T),
}

// apply: the wrapped function is stored in Maybe<F>
// If either is Nothing, the whole thing is Nothing
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,
     }
 }
}

// Usage:
let double = Maybe::Just(|x: i32| x * 2);
let five   = Maybe::Just(5);
println!("{:?}", double.apply(five)); // Just(10)

let no_fn: Maybe<fn(i32) -> i32> = Maybe::Nothing;
println!("{:?}", no_fn.apply(five)); // Nothing โ€” function was absent
// lift2: combine two independent Maybe values with a plain function
// No currying needed โ€” Rust takes multi-argument closures 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,  // Either was absent โ€” result is absent
 }
}

// lift3: same idea for three independent values
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,
 }
}
// Real example: parse two numbers and add them
let sum = lift2_simple(
 |a, b| a + b,
 parse_int("42"),   // Maybe::Just(42)
 parse_int("bad"),  // Maybe::Nothing
);
// Result: Maybe::Nothing  โ€” one bad parse, result is Nothing
// Rust's Option already has zip() as a built-in applicative combinator:
let a = "42".parse::<i32>().ok();
let b = "7".parse::<i32>().ok();
let pair = a.zip(b); // Some((42, 7))
// zip gives you the pair; you still need map to apply the function after
let sum = a.zip(b).map(|(x, y)| x + y); // Some(49)
Note on currying: OCaml functions are curried by default, which lets you write elegant `pure f <> a <> b` chains. Rust doesn't curry, so `lift2`/`lift3` take the full multi-argument closure directly. This is actually cleaner for most Rust use cases.

What This Unlocks

Key Differences

ConceptOCamlRust
Syntax`pure f <> a <> b` (infix operators)`lift2_simple(f, a, b)` (free functions)
CurryingFunctions are curried by default; `f a` returns a functionNo currying; closures take all args at once
Built-in applicativeNo `zip` equivalent in stdlib`Option::zip` is a built-in applicative combinator
Wrapped function`Maybe<'a -> 'b>` โ€” function is a value`Maybe<F> where F: FnOnce(A) -> B` โ€” needs generic bounds
`pure``pure x` wraps a value`Maybe::Just(x)` or `Some(x)`
// 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);
    }
}
(* Example 053: Applicative Functor Basics *)
(* Applicative: apply a wrapped function to a wrapped value *)

type 'a maybe = Nothing | Just of 'a

let map f = function Nothing -> Nothing | Just x -> Just (f x)
let pure x = Just x
let apply mf mx = match mf with
  | Nothing -> Nothing
  | Just f -> map f mx

(* Infix operators *)
let ( <$> ) f x = map f x
let ( <*> ) = apply

(* Approach 1: Apply function in context *)
let add x y = x + y

let result1 = (pure add) <*> (Just 3) <*> (Just 4)
(* = Just 7 *)

(* Approach 2: Lifting a multi-argument function *)
let lift2 f a b = (pure f) <*> a <*> b
let lift3 f a b c = (pure f) <*> a <*> b <*> c

let concat3 a b c = a ^ b ^ c

(* Approach 3: Using applicative for independent computations *)
let parse_int s = try Just (int_of_string s) with _ -> Nothing
let parse_float s = try Just (float_of_string s) with _ -> Nothing

let make_pair x y = (x, y)

let () =
  (* Basic apply *)
  assert (result1 = Just 7);
  assert (apply (Just (fun x -> x * 2)) (Just 5) = Just 10);
  assert (apply Nothing (Just 5) = Nothing);
  assert (apply (Just (fun x -> x * 2)) Nothing = Nothing);

  (* lift2 *)
  assert (lift2 add (Just 10) (Just 20) = Just 30);
  assert (lift2 add Nothing (Just 20) = Nothing);

  (* lift3 *)
  assert (lift3 concat3 (Just "a") (Just "b") (Just "c") = Just "abc");

  (* Independent parsing *)
  let pair = lift2 make_pair (parse_int "42") (parse_int "7") in
  assert (pair = Just (42, 7));
  let pair2 = lift2 make_pair (parse_int "bad") (parse_int "7") in
  assert (pair2 = Nothing);

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Applicative Functor Basics

Apply Operation

OCaml:

๐Ÿช Show OCaml equivalent
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:

๐Ÿช Show OCaml equivalent
(* 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)