ExamplesBy LevelBy TopicLearning Paths
605 Intermediate

Applicative Laws

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Applicative Laws" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Applicative functors sit between functors and monads — they enable combining multiple independent computations. Key difference from OCaml: 1. **HKT requirement**: These morphisms ideally require higher

Tutorial

The Problem

Applicative functors sit between functors and monads — they enable combining multiple independent computations. Four laws: identity, homomorphism, interchange, and composition. Option and Result are applicatives: applying a wrapped function to a wrapped value. Applicatives enable parallel computation that monads cannot (monad bind is sequential; applicative apply can be parallel).

🎯 Learning Outcomes

  • • The categorical definition and the mathematical laws that must hold
  • • How to implement this pattern in Rust despite the lack of higher-kinded types
  • • The relationship to more familiar functional idioms (fold, unfold, map)
  • • Key concepts: pure, apply (ap), four applicative laws
  • • Where this pattern appears in production systems and when simpler alternatives suffice
  • Code Example

    #![allow(clippy::all)]
    //! # Applicative Laws
    //! Applicatives extend Functors with pure and apply.
    
    pub trait Applicative<A>: Sized {
        fn pure(a: A) -> Self;
        fn ap<B>(self, f: Self) -> Self
        where
            Self: Sized;
    }
    
    impl<A> Applicative<A> for Option<A> {
        fn pure(a: A) -> Self {
            Some(a)
        }
        fn ap<B>(self, _f: Self) -> Self {
            self
        }
    }
    
    pub fn identity_law<A: Clone + PartialEq>(v: Option<A>) -> bool {
        let mapped = v.clone().map(|x| x);
        mapped == v
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_pure() {
            assert_eq!(Option::pure(42), Some(42));
        }
    }

    Key Differences

  • HKT requirement: These morphisms ideally require higher-kinded types for full generality; Rust uses GATs or associated types as approximations.
  • Performance: Rust's implementations are more verbose but compile to efficient machine code; OCaml's implementations are more concise with similar runtime performance.
  • Practical adoption: In Haskell, recursive schemes from recursion-schemes are widely used; in Rust and OCaml, direct recursion is more common in practice.
  • Theoretical value: Understanding these patterns deepens intuition for all recursive programming, even when direct recursion is used in production code.
  • OCaml Approach

    OCaml's pattern matching and recursive types make morphism implementations natural. The Fix type and F-algebra/coalgebra patterns translate directly, though without Haskell's typeclass machinery:

    (* OCaml recursive schemes use:
       - Recursive variant types for F-algebras
       - Higher-order functions for the morphism
       - GADTs for type-safe fixed points in advanced cases *)
    

    Full Source

    #![allow(clippy::all)]
    //! # Applicative Laws
    //! Applicatives extend Functors with pure and apply.
    
    pub trait Applicative<A>: Sized {
        fn pure(a: A) -> Self;
        fn ap<B>(self, f: Self) -> Self
        where
            Self: Sized;
    }
    
    impl<A> Applicative<A> for Option<A> {
        fn pure(a: A) -> Self {
            Some(a)
        }
        fn ap<B>(self, _f: Self) -> Self {
            self
        }
    }
    
    pub fn identity_law<A: Clone + PartialEq>(v: Option<A>) -> bool {
        let mapped = v.clone().map(|x| x);
        mapped == v
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_pure() {
            assert_eq!(Option::pure(42), Some(42));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_pure() {
            assert_eq!(Option::pure(42), Some(42));
        }
    }

    Deep Comparison

    Applicative Laws

  • • Identity: pure id <*> v == v
  • • Composition: pure (.) <> u <> v <> w == u <> (v <*> w)
  • • Homomorphism: pure f <*> pure x == pure (f x)
  • • Interchange: u <> pure y == pure ($ y) <> u
  • Exercises

  • Laws verification: Write tests that verify the categorical laws for this morphism on a specific data type.
  • New data type: Apply the morphism to a different recursive data type (e.g., apply catamorphism to a rose tree instead of a binary tree).
  • Comparison: Implement the same computation using direct recursion and the morphism — measure whether the morphism version composes more cleanly.
  • Open Source Repos