ExamplesBy LevelBy TopicLearning Paths
509 Intermediate

Closure Composition

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Composition" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Complex data transformations are best expressed as a sequence of simple steps: parse → validate → normalise → format. Key difference from OCaml: 1. **Built

Tutorial

The Problem

Complex data transformations are best expressed as a sequence of simple steps: parse → validate → normalise → format. Manually nesting function calls f(g(h(x))) becomes unreadable for long chains. Function composition formalises this: compose(f, g) returns a new function that applies g then f. Piping (|> in F#, OCaml, and Elixir) applies left-to-right. The Pipeline builder pattern extends this to dynamic lists of transformations.

🎯 Learning Outcomes

  • • Implement compose(f, g) returning impl Fn(A) -> C for mathematical f ∘ g
  • • Implement pipe(f, g) for left-to-right f | g composition
  • • Build make_pipeline(Vec<Box<dyn Fn(T)->T>>) for dynamic chain construction
  • • Use the Pipeline builder with fluent .then(f).then(g).run() API
  • • Understand the type constraints: F: Fn(A)->B, G: Fn(B)->C for pipe(F, G)
  • Code Example

    fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where F: Fn(B) -> C, G: Fn(A) -> B {
        move |x| f(g(x))
    }
    
    let pipeline = compose(square, compose(inc, double));
    let result = pipeline(3);  // 49

    Key Differences

  • Built-in operators: OCaml has |> and @@ in the standard library; Rust has no built-in composition operators — they are library functions.
  • Type inference: OCaml infers all types in compose/pipe; Rust requires explicit type parameters <A, B, C, F, G> for composition functions.
  • Dynamic pipeline: Rust's make_pipeline(Vec<Box<dyn Fn(T)->T>>) requires boxing (heap allocation); OCaml's List.fold_left over function lists uses uniform representation.
  • Builder pattern: Rust's Pipeline::new().then(f).then(g).run() consumes self at each step (move semantics); OCaml would use a mutable list ref or a functional accumulator.
  • OCaml Approach

    OCaml has @@ (right-to-left application) and |> (left-to-right pipe) built in:

    let compose f g x = f (g x)   (* right-to-left *)
    let pipe f g x = g (f x)      (* left-to-right *)
    
    (* Using built-in operators *)
    let result = 5 |> (fun x -> x * 2) |> (fun x -> x + 1)  (* 11 *)
    
    (* Dynamic pipeline *)
    let make_pipeline transforms x =
      List.fold_left (fun acc f -> f acc) x transforms
    

    OCaml 4.01 added |> and @@ to the standard library; they are idiomatic for function pipelines.

    Full Source

    #![allow(clippy::all)]
    //! Function Composition
    //!
    //! Building complex transformations from simple composed pieces.
    
    /// Compose two functions: apply g first, then f.
    /// compose(f, g)(x) == f(g(x))
    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(B) -> C,
        G: Fn(A) -> B,
    {
        move |x| f(g(x))
    }
    
    /// Pipe: apply f first, then g (left-to-right composition).
    pub fn pipe<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(A) -> B,
        G: Fn(B) -> C,
    {
        move |x| g(f(x))
    }
    
    /// Build a pipeline from a Vec of boxed transformations.
    pub fn make_pipeline<T>(transforms: Vec<Box<dyn Fn(T) -> T>>) -> impl Fn(T) -> T {
        move |x| transforms.iter().fold(x, |acc, f| f(acc))
    }
    
    /// A builder that accumulates transformations.
    pub struct Pipeline<T> {
        steps: Vec<Box<dyn Fn(T) -> T>>,
    }
    
    impl<T: 'static> Pipeline<T> {
        pub fn new() -> Self {
            Pipeline { steps: Vec::new() }
        }
    
        pub fn then(mut self, f: impl Fn(T) -> T + 'static) -> Self {
            self.steps.push(Box::new(f));
            self
        }
    
        pub fn run(self) -> impl Fn(T) -> T {
            make_pipeline(self.steps)
        }
    }
    
    impl<T: 'static> Default for Pipeline<T> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compose_basic() {
            let f = compose(|x: i32| x + 1, |x| x * 2);
            assert_eq!(f(5), 11); // (5*2)+1 = 11
        }
    
        #[test]
        fn test_pipe_basic() {
            let f = pipe(|x: i32| x * 2, |x| x + 1);
            assert_eq!(f(5), 11); // (5*2)+1 = 11
        }
    
        #[test]
        fn test_compose_vs_pipe_order() {
            let double = |x: i32| x * 2;
            let inc = |x: i32| x + 1;
    
            // compose: right-to-left (inc after double)
            let c = compose(inc, double);
            // pipe: left-to-right (double then inc)
            let p = pipe(double, inc);
    
            assert_eq!(c(3), p(3)); // both: (3*2)+1 = 7
        }
    
        #[test]
        fn test_pipeline_builder() {
            let p = Pipeline::new().then(|x: i32| x + 1).then(|x| x * 3).run();
            assert_eq!(p(4), 15); // (4+1)*3 = 15
        }
    
        #[test]
        fn test_identity_compose() {
            let f = compose(|x: i32| x, |x| x);
            assert_eq!(f(42), 42);
        }
    
        #[test]
        fn test_compose_type_change() {
            let to_string = compose(|s: String| s.len(), |x: i32| x.to_string());
            assert_eq!(to_string(12345), 5);
        }
    
        #[test]
        fn test_triple_compose() {
            let double = |x: i32| x * 2;
            let inc = |x: i32| x + 1;
            let square = |x: i32| x * x;
    
            let f = compose(square, compose(inc, double));
            assert_eq!(f(3), 49); // ((3*2)+1)^2 = 49
        }
    
        #[test]
        fn test_make_pipeline() {
            let transforms: Vec<Box<dyn Fn(i32) -> i32>> = vec![
                Box::new(|x| x * 2),
                Box::new(|x| x + 1),
                Box::new(|x| x * x),
            ];
            let pipeline = make_pipeline(transforms);
            assert_eq!(pipeline(2), 25); // ((2*2)+1)^2 = 25
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compose_basic() {
            let f = compose(|x: i32| x + 1, |x| x * 2);
            assert_eq!(f(5), 11); // (5*2)+1 = 11
        }
    
        #[test]
        fn test_pipe_basic() {
            let f = pipe(|x: i32| x * 2, |x| x + 1);
            assert_eq!(f(5), 11); // (5*2)+1 = 11
        }
    
        #[test]
        fn test_compose_vs_pipe_order() {
            let double = |x: i32| x * 2;
            let inc = |x: i32| x + 1;
    
            // compose: right-to-left (inc after double)
            let c = compose(inc, double);
            // pipe: left-to-right (double then inc)
            let p = pipe(double, inc);
    
            assert_eq!(c(3), p(3)); // both: (3*2)+1 = 7
        }
    
        #[test]
        fn test_pipeline_builder() {
            let p = Pipeline::new().then(|x: i32| x + 1).then(|x| x * 3).run();
            assert_eq!(p(4), 15); // (4+1)*3 = 15
        }
    
        #[test]
        fn test_identity_compose() {
            let f = compose(|x: i32| x, |x| x);
            assert_eq!(f(42), 42);
        }
    
        #[test]
        fn test_compose_type_change() {
            let to_string = compose(|s: String| s.len(), |x: i32| x.to_string());
            assert_eq!(to_string(12345), 5);
        }
    
        #[test]
        fn test_triple_compose() {
            let double = |x: i32| x * 2;
            let inc = |x: i32| x + 1;
            let square = |x: i32| x * x;
    
            let f = compose(square, compose(inc, double));
            assert_eq!(f(3), 49); // ((3*2)+1)^2 = 49
        }
    
        #[test]
        fn test_make_pipeline() {
            let transforms: Vec<Box<dyn Fn(i32) -> i32>> = vec![
                Box::new(|x| x * 2),
                Box::new(|x| x + 1),
                Box::new(|x| x * x),
            ];
            let pipeline = make_pipeline(transforms);
            assert_eq!(pipeline(2), 25); // ((2*2)+1)^2 = 25
        }
    }

    Deep Comparison

    OCaml vs Rust: Function Composition

    OCaml

    let compose f g x = f (g x)
    let ( >> ) g f x = f (g x)  (* pipe operator *)
    
    let pipeline = double >> inc >> square
    let result = pipeline 3  (* 49 *)
    

    Rust

    fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where F: Fn(B) -> C, G: Fn(A) -> B {
        move |x| f(g(x))
    }
    
    let pipeline = compose(square, compose(inc, double));
    let result = pipeline(3);  // 49
    

    Key Differences

  • OCaml: Custom operators >> and << make composition readable
  • Rust: Generic compose function with explicit type bounds
  • OCaml: Currying makes composition natural
  • Rust: Move closures needed to own captured functions
  • Both support building complex transforms from simple pieces
  • Exercises

  • N-ary compose: Write fn compose_all<T>(fns: Vec<Box<dyn Fn(T)->T>>) -> impl Fn(T)->T that composes a list right-to-left (last function applied first).
  • Typed pipeline: Design a type-safe pipeline where each step's output type must match the next step's input type — use a struct Pipeline<A, B> parameterised by input and output types.
  • Lazy evaluation: Wrap the Pipeline in a struct LazyPipeline<T> that stores the input alongside the transforms and evaluates lazily when .evaluate() is called.
  • Open Source Repos