Applicative Functor Basics
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
pure(x) = Just(x): lifting a plain value into the applicative contextapply(mf, mx): apply a wrapped function to a wrapped valueOption values, two Result values without early-exit chainingCode 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
| Aspect | Rust | OCaml |
|---|---|---|
pure | Maybe::pure(x) | pure x or Some x |
apply | .apply(mf) method | <*> infix operator |
| Currying | Manual (closures returning closures) | Automatic |
| Multi-arg combine | zip or nested apply | f <*> mx <*> my |
| HKT limitation | Cannot express generic Applicative | Module functor can |
| Independent effects | Yes (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);
}
}#[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
map2(f, mx, my) for Maybe using apply and pure: combine two independent Maybes with a binary function.pure(id).apply(mx) == mx.pure(f).apply(pure(x)) == pure(f(x)).Result<T, E>: both values must be Ok; if either is Err, return the first Err.applicative(f, option1, option2) cannot express "parse field2 differently based on field1's value."