ExamplesBy LevelBy TopicLearning Paths
003 Fundamental

Pipeline Operator

Higher-Order FunctionsFunction Composition

Tutorial Video

Text description (accessibility)

This video demonstrates the "Pipeline Operator" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Higher-Order Functions, Function Composition. The pipeline operator `|>` is a function composition tool that threads a value through a series of transformations from left-to-right. Key difference from OCaml: 1. **Syntax:** OCaml's `|>` is syntactic sugar for right

Tutorial

The Problem

The pipeline operator |> is a function composition tool that threads a value through a series of transformations from left-to-right. In OCaml, it's defined simply as let (|>) x f = f x, which applies function f to value x. This example demonstrates how to chain function calls in a readable sequence.

Without a pipeline operator, deeply nested function calls read inside-out: h(g(f(x))) requires the reader to start in the middle and work outward. This is cognitively harder than left-to-right reading. Unix pipes solve this for shell commands, and React's component composition solves it for UI transforms. OCaml's |> operator is the functional programming solution: x |> f |> g |> h reads in the order of execution.

🎯 Learning Outcomes

  • • Understand function application and composition in Rust vs OCaml
  • • Learn three approaches to chaining transformations: nested calls, pipe functions, and function composition
  • • Recognize that Rust's method chaining is the idiomatic equivalent to OCaml's pipe operator
  • • See how FnOnce, closures, and trait bounds enable higher-order programming in Rust
  • 🦀 The Rust Way

    Rust achieves similar composability through several idiomatic patterns: nested function calls for simple cases, the pipe function pattern for explicit composition, and function composition combinators. While Rust doesn't have a built-in |> operator, the same semantics are easily expressed using higher-order functions and closures.

    Code Example

    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    // Idiomatic Rust: direct function composition (nested calls)
    pub fn compute_result_idiomatic() -> i32 {
        add_one(double(5))  // 11
    }
    
    pub fn shout(s: &str) -> String {
        s.to_uppercase()
    }
    
    pub fn add_exclaim(s: &str) -> String {
        format!("{}!", s)
    }
    
    pub fn compute_greeting_idiomatic() -> String {
        add_exclaim(&shout("hello"))  // "HELLO!"
    }

    Key Differences

  • Syntax: OCaml's |> is syntactic sugar for right-associative function application. Rust requires explicit function calls or a custom pipe function.
  • Method Chaining: Rust's idiomatic style uses method chaining (.map(), .filter(), etc.), which is the natural equivalent. OCaml doesn't have methods on built-in types.
  • Ownership: Rust's pipe function uses FnOnce for one-shot transformations, enforcing move semantics when values are consumed. OCaml handles this implicitly.
  • Type Inference: Both languages infer the intermediate types in a pipeline, but Rust requires explicit type bounds for generic higher-order functions.
  • Syntax: OCaml's |> is a built-in infix operator. Rust has no equivalent — closest is method chaining for iterators.
  • Method chaining: Rust's idiomatic style uses method chaining (.map(), .filter(), etc.), which is the natural equivalent for collection pipelines. OCaml doesn't have methods on built-in types.
  • Ownership: Rust's pipe function uses FnOnce for one-shot transformations, enforcing move semantics when values are consumed. OCaml handles this implicitly via garbage collection.
  • Type inference: Both languages infer intermediate types in pipelines, but Rust sometimes needs explicit type annotations when the compiler cannot determine the output type of a chain.
  • OCaml Approach

    OCaml's |> operator provides a natural left-to-right reading order for chained transformations. It's defined as a simple infix operator that applies the right-hand function to the left-hand value. This makes complex pipelines easy to read: 5 |> double |> add_one reads as "take 5, double it, add one".

    Full Source

    #![allow(clippy::all)]
    // Pure functions for integers
    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    // Pure functions for strings
    pub fn shout(s: &str) -> String {
        s.to_uppercase()
    }
    
    pub fn add_exclaim(s: &str) -> String {
        format!("{}!", s)
    }
    
    // Idiomatic Rust: direct function composition
    // This is the natural way to compose functions in Rust.
    // We apply functions left-to-right by nesting function calls.
    pub fn compute_result_idiomatic() -> i32 {
        add_one(double(5)) // 11
    }
    
    pub fn compute_greeting_idiomatic() -> String {
        add_exclaim(&shout("hello")) // "HELLO!"
    }
    
    // Functional Rust: pipe function (mimics OCaml's |>)
    // Takes a value and applies a function to it.
    // In OCaml: let (|>) x f = f x
    // This shows the explicit function application order.
    pub fn pipe<T, U>(value: T, f: impl FnOnce(T) -> U) -> U {
        f(value)
    }
    
    // Using pipe with nested calls to show the transformation chain
    pub fn compute_result_with_pipe() -> i32 {
        pipe(pipe(5, double), add_one) // 11
    }
    
    pub fn compute_greeting_with_pipe() -> String {
        let shouted = pipe("hello", shout);
        pipe(&shouted, |s| add_exclaim(s)) // "HELLO!"
    }
    
    // Composition function: creates a new function from two functions
    // This shows function composition, another way to chain transformations
    pub fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
        move |x| g(f(x))
    }
    
    pub fn compute_result_with_composition() -> i32 {
        let double_then_add_one = compose(double, add_one);
        double_then_add_one(5) // 11
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_double() {
            assert_eq!(double(5), 10);
            assert_eq!(double(0), 0);
        }
    
        #[test]
        fn test_add_one() {
            assert_eq!(add_one(5), 6);
            assert_eq!(add_one(0), 1);
        }
    
        #[test]
        fn test_compute_result_idiomatic() {
            assert_eq!(compute_result_idiomatic(), 11);
        }
    
        #[test]
        fn test_compute_result_with_pipe() {
            assert_eq!(compute_result_with_pipe(), 11);
        }
    
        #[test]
        fn test_compute_result_with_composition() {
            assert_eq!(compute_result_with_composition(), 11);
        }
    
        #[test]
        fn test_compute_greeting_idiomatic() {
            assert_eq!(compute_greeting_idiomatic(), "HELLO!");
        }
    
        #[test]
        fn test_compute_greeting_with_pipe() {
            assert_eq!(compute_greeting_with_pipe(), "HELLO!");
        }
    
        #[test]
        fn test_pipe_with_closures() {
            let result = pipe(5, |x| x * 2);
            assert_eq!(result, 10);
        }
    
        #[test]
        fn test_shout() {
            assert_eq!(shout("hello"), "HELLO");
            assert_eq!(shout("world"), "WORLD");
        }
    
        #[test]
        fn test_add_exclaim() {
            assert_eq!(add_exclaim("HELLO"), "HELLO!");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_double() {
            assert_eq!(double(5), 10);
            assert_eq!(double(0), 0);
        }
    
        #[test]
        fn test_add_one() {
            assert_eq!(add_one(5), 6);
            assert_eq!(add_one(0), 1);
        }
    
        #[test]
        fn test_compute_result_idiomatic() {
            assert_eq!(compute_result_idiomatic(), 11);
        }
    
        #[test]
        fn test_compute_result_with_pipe() {
            assert_eq!(compute_result_with_pipe(), 11);
        }
    
        #[test]
        fn test_compute_result_with_composition() {
            assert_eq!(compute_result_with_composition(), 11);
        }
    
        #[test]
        fn test_compute_greeting_idiomatic() {
            assert_eq!(compute_greeting_idiomatic(), "HELLO!");
        }
    
        #[test]
        fn test_compute_greeting_with_pipe() {
            assert_eq!(compute_greeting_with_pipe(), "HELLO!");
        }
    
        #[test]
        fn test_pipe_with_closures() {
            let result = pipe(5, |x| x * 2);
            assert_eq!(result, 10);
        }
    
        #[test]
        fn test_shout() {
            assert_eq!(shout("hello"), "HELLO");
            assert_eq!(shout("world"), "WORLD");
        }
    
        #[test]
        fn test_add_exclaim() {
            assert_eq!(add_exclaim("HELLO"), "HELLO!");
        }
    }

    Deep Comparison

    OCaml vs Rust: Pipeline Operator

    The pipeline operator is a simple but powerful tool for making function composition readable. In OCaml, it's a lightweight operator that enables left-to-right function application. In Rust, we achieve the same result through different idiomatic patterns.

    Side-by-Side Code

    OCaml

    (* The pipeline operator is just a higher-order function *)
    let ( |> ) x f = f x
    
    let double x = 2 * x
    let add1 x = x + 1
    
    (* Read: start with 5, double it, add 1 *)
    let result = 5 |> double |> add1   (* 11 *)
    
    (* Chaining string operations *)
    let shout s = String.uppercase_ascii s
    let exclaim s = s ^ "!"
    
    let greeting = "hello" |> shout |> exclaim   (* "HELLO!" *)
    

    Rust (idiomatic)

    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    // Idiomatic Rust: direct function composition (nested calls)
    pub fn compute_result_idiomatic() -> i32 {
        add_one(double(5))  // 11
    }
    
    pub fn shout(s: &str) -> String {
        s.to_uppercase()
    }
    
    pub fn add_exclaim(s: &str) -> String {
        format!("{}!", s)
    }
    
    pub fn compute_greeting_idiomatic() -> String {
        add_exclaim(&shout("hello"))  // "HELLO!"
    }
    

    Rust (functional/pipe)

    // Functional Rust: explicit pipe function (mimics OCaml's |>)
    pub fn pipe<T, U>(value: T, f: impl FnOnce(T) -> U) -> U {
        f(value)
    }
    
    pub fn compute_result_with_pipe() -> i32 {
        pipe(pipe(5, double), add_one)  // 11
    }
    
    pub fn compute_greeting_with_pipe() -> String {
        let shouted = pipe("hello", shout);
        pipe(&shouted, |s| add_exclaim(s))  // "HELLO!"
    }
    

    Type Signatures

    ConceptOCamlRust
    Pipeline operatorval (|>) : 'a -> ('a -> 'b) -> 'bfn pipe<T, U>(value: T, f: impl FnOnce(T) -> U) -> U
    Integer transformationint -> intfn(i32) -> i32
    String transformationstring -> stringfn(&str) -> String
    Composed functionImplicit through \|>Explicit via compose trait or nested calls

    Key Insights

  • Function Application vs Syntax: OCaml's |> is syntactic sugar for simple function application. Rust achieves the same through nested function calls or an explicit pipe function. This shows that operators are just functions in different syntax.
  • Ownership and Borrowing: The Rust pipe function uses FnOnce to accept a function that consumes its input. This is Rust's way of enforcing that each transformation takes ownership of the value. OCaml handles this implicitly without explicit ownership semantics.
  • Method Chaining vs Operators: In idiomatic Rust, the preferred approach for many pipelines is method chaining (.map(), .filter(), etc.), which reads left-to-right like |>. OCaml doesn't have methods on built-in types, so |> is the idiomatic solution there.
  • Closures for Conversion: When piping a String to a function expecting &str, Rust requires an explicit closure (|s| add_exclaim(s)) to handle type conversion. OCaml's implicit coercion would handle this automatically, showing a difference in type system strictness.
  • Generics and Trait Bounds: Rust's pipe function is generic over both input and output types with impl FnOnce(T) -> U, enabling type-safe composition without runtime overhead. OCaml's polymorphic 'a -> ('a -> 'b) -> 'b achieves the same with implicit polymorphism.
  • When to Use Each Style

    Use idiomatic Rust nested calls when:

  • • Composing two or three functions (e.g., f(g(h(x)))).
  • • Working with iterator chains (.map(), .filter(), etc.), which are left-to-right and naturally readable.
  • • The composition fits naturally on one line or is part of a larger expression.
  • Use the pipe function in Rust when:

  • • You want to explicitly show function application order similar to OCaml's |>.
  • • Demonstrating functional programming concepts or translating OCaml code directly.
  • • Chaining many custom functions where the pipe notation improves readability over nested calls.
  • Use function composition in Rust when:

  • • Creating reusable composed functions (e.g., a function that doubles then adds one).
  • • Higher-order programming where the composition itself becomes a parameter.
  • • You want to name intermediate transformations for clarity.
  • Syntactic Observations

    OCaml's 5 |> double |> add_one reads perfectly left-to-right. Rust's equivalent nested form add_one(double(5)) reads right-to-left (inside-out), which is why method chaining and the pipe function are often preferred for readability. This is a key difference in expressiveness: OCaml's operator syntax makes the data flow obvious, while Rust requires more deliberate structuring to achieve the same clarity.

    Exercises

  • Define a pipe2 macro (or function pair) that chains two single-argument closures and apply it to a string processing pipeline: parse → validate → format.
  • Extend the pipeline pattern to support error propagation: write pipe_result that threads a Result<T, E> through a sequence of FnOnce(T) -> Result<U, E> steps, short-circuiting on the first error.
  • Build a numeric pipeline using pipe that computes a statistical summary (min, max, mean, standard deviation) over a Vec<f64> in a single readable chain.
  • Logging pipeline: Add logging to each pipeline stage: log_stage(name, f) wraps any function so that it prints the input and output to stderr before forwarding the result.
  • Error pipeline: Implement result_pipe<T, U, E>(value: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> — a pipe operator for fallible operations.
  • Open Source Repos