ExamplesBy LevelBy TopicLearning Paths
526 Intermediate

Pipe Operator Simulation

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Pipe Operator Simulation" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. OCaml's `|>` (pipe) operator, F#'s `|>`, and Elixir's `|>` all solve the same readability problem: deeply nested function calls read inside-out, but data transformations are conceptually left-to-right. Key difference from OCaml: 1. **Language support**: OCaml has `|>` as a stdlib operator available everywhere; Rust requires either an extension trait (library

Tutorial

The Problem

OCaml's |> (pipe) operator, F#'s |>, and Elixir's |> all solve the same readability problem: deeply nested function calls read inside-out, but data transformations are conceptually left-to-right. f(g(h(x))) is hard to read; x |> h |> g |> f reads as a pipeline. Rust does not have a native pipe operator, but the pattern can be simulated with an extension trait Pipe that adds .pipe(f) to every type. This lets Rust code express transformation pipelines in the same left-to-right style as functional languages.

🎯 Learning Outcomes

  • • How to implement |> semantics using an extension trait with a blanket impl
  • • How pipe, pipe_ref, and pipe_mut handle different ownership scenarios
  • • How function composition with compose and compose_n relates to piping
  • • Where the pipe pattern appears: data transformation, validation chains, error propagation
  • • Why Rust does not have a native pipe operator and what RFC proposals exist
  • Code Example

    pub trait Pipe: Sized {
        fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B { f(self) }
    }
    impl<T> Pipe for T {}
    
    // Usage
    let result = 5.pipe(double).pipe(add1).pipe(square);  // 121
    
    // Type change
    let result = 42.pipe(to_string).pipe(prefix);

    Key Differences

  • Language support: OCaml has |> as a stdlib operator available everywhere; Rust requires either an extension trait (library-level) or the nightly |> RFC (not yet stabilized).
  • Ownership variants: Rust needs three pipe variants (pipe, pipe_ref, pipe_mut) for owned, borrowed, and mutable cases; OCaml has one |> since all values are GC-managed.
  • Composition: Rust's compose returns impl Fn — an anonymous type; OCaml function composition with >> or @@ returns a plain function value visible to the type system.
  • Inline cost: Rust's blanket impl<T> Pipe for T is always inlined at zero cost; OCaml's |> is a regular function call, optimized away by the compiler in most cases.
  • OCaml Approach

    OCaml has |> as a built-in operator in the standard library since OCaml 4.01. It is simply defined as let (|>) x f = f x. No extension traits or special syntax are needed — it is universally available and composes with every function.

    5 |> double |> add1 |> square |> string_of_int |> (fun s -> "Result: " ^ s)
    

    Full Source

    #![allow(clippy::all)]
    //! Pipe Operator Simulation
    //!
    //! Simulating OCaml's |> operator with a Pipe extension trait.
    
    /// Extension trait to simulate the |> pipe operator.
    pub trait Pipe: Sized {
        /// Apply f to self: self.pipe(f) == f(self)
        fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B {
            f(self)
        }
    
        /// pipe_ref: apply f to &self (doesn't consume)
        fn pipe_ref<B, F: FnOnce(&Self) -> B>(&self, f: F) -> B {
            f(self)
        }
    
        /// pipe_mut: apply f to &mut self
        fn pipe_mut<B, F: FnOnce(&mut Self) -> B>(&mut self, f: F) -> B {
            f(self)
        }
    }
    
    impl<T> Pipe for T {}
    
    /// Some functions to use with pipe.
    pub fn double(x: i32) -> i32 {
        x * 2
    }
    
    pub fn add1(x: i32) -> i32 {
        x + 1
    }
    
    pub fn square(x: i32) -> i32 {
        x * x
    }
    
    pub fn to_string(x: i32) -> String {
        x.to_string()
    }
    
    pub fn prefix(s: String) -> String {
        format!("Result: {}", s)
    }
    
    /// Compose two functions into one.
    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(A) -> B,
        G: Fn(B) -> C,
    {
        move |a| g(f(a))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_pipe_single() {
            let result = 5.pipe(double);
            assert_eq!(result, 10);
        }
    
        #[test]
        fn test_pipe_chain() {
            // 5 -> double -> add1 -> square
            // 5 -> 10 -> 11 -> 121
            let result = 5.pipe(double).pipe(add1).pipe(square);
            assert_eq!(result, 121);
        }
    
        #[test]
        fn test_pipe_type_change() {
            // 42 -> to_string -> prefix
            let result = 42.pipe(to_string).pipe(prefix);
            assert_eq!(result, "Result: 42");
        }
    
        #[test]
        fn test_pipe_with_closure() {
            let offset = 100;
            let result = 5.pipe(|x| x + offset).pipe(|x| x * 2);
            assert_eq!(result, 210); // (5 + 100) * 2
        }
    
        #[test]
        fn test_pipe_ref() {
            let v = vec![1, 2, 3, 4, 5];
            let sum = v.pipe_ref(|v| v.iter().sum::<i32>());
            assert_eq!(sum, 15);
            // v is still usable
            assert_eq!(v.len(), 5);
        }
    
        #[test]
        fn test_pipe_mut() {
            let mut v = vec![1, 2, 3];
            v.pipe_mut(|v| v.push(4));
            assert_eq!(v, vec![1, 2, 3, 4]);
        }
    
        #[test]
        fn test_pipe_with_methods() {
            let result = "  hello  ".pipe(|s| s.trim()).pipe(|s| s.to_uppercase());
            assert_eq!(result, "HELLO");
        }
    
        #[test]
        fn test_compose_basic() {
            let double_then_add1 = compose(double, add1);
            assert_eq!(double_then_add1(5), 11); // 5*2 + 1
        }
    
        #[test]
        fn test_compose_chain() {
            let pipeline = compose(compose(double, add1), square);
            assert_eq!(pipeline(5), 121); // ((5*2)+1)^2
        }
    
        #[test]
        fn test_pipe_vs_method_chain() {
            // Traditional method chain
            let v1: Vec<i32> = vec![1, 2, 3, 4, 5]
                .into_iter()
                .map(|x| x * 2)
                .filter(|x| *x > 4)
                .collect();
    
            // With pipe (collecting intermediate)
            let v2 = vec![1, 2, 3, 4, 5]
                .pipe(|v| v.into_iter().map(|x| x * 2).collect::<Vec<_>>())
                .pipe(|v| v.into_iter().filter(|x| *x > 4).collect::<Vec<_>>());
    
            assert_eq!(v1, v2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_pipe_single() {
            let result = 5.pipe(double);
            assert_eq!(result, 10);
        }
    
        #[test]
        fn test_pipe_chain() {
            // 5 -> double -> add1 -> square
            // 5 -> 10 -> 11 -> 121
            let result = 5.pipe(double).pipe(add1).pipe(square);
            assert_eq!(result, 121);
        }
    
        #[test]
        fn test_pipe_type_change() {
            // 42 -> to_string -> prefix
            let result = 42.pipe(to_string).pipe(prefix);
            assert_eq!(result, "Result: 42");
        }
    
        #[test]
        fn test_pipe_with_closure() {
            let offset = 100;
            let result = 5.pipe(|x| x + offset).pipe(|x| x * 2);
            assert_eq!(result, 210); // (5 + 100) * 2
        }
    
        #[test]
        fn test_pipe_ref() {
            let v = vec![1, 2, 3, 4, 5];
            let sum = v.pipe_ref(|v| v.iter().sum::<i32>());
            assert_eq!(sum, 15);
            // v is still usable
            assert_eq!(v.len(), 5);
        }
    
        #[test]
        fn test_pipe_mut() {
            let mut v = vec![1, 2, 3];
            v.pipe_mut(|v| v.push(4));
            assert_eq!(v, vec![1, 2, 3, 4]);
        }
    
        #[test]
        fn test_pipe_with_methods() {
            let result = "  hello  ".pipe(|s| s.trim()).pipe(|s| s.to_uppercase());
            assert_eq!(result, "HELLO");
        }
    
        #[test]
        fn test_compose_basic() {
            let double_then_add1 = compose(double, add1);
            assert_eq!(double_then_add1(5), 11); // 5*2 + 1
        }
    
        #[test]
        fn test_compose_chain() {
            let pipeline = compose(compose(double, add1), square);
            assert_eq!(pipeline(5), 121); // ((5*2)+1)^2
        }
    
        #[test]
        fn test_pipe_vs_method_chain() {
            // Traditional method chain
            let v1: Vec<i32> = vec![1, 2, 3, 4, 5]
                .into_iter()
                .map(|x| x * 2)
                .filter(|x| *x > 4)
                .collect();
    
            // With pipe (collecting intermediate)
            let v2 = vec![1, 2, 3, 4, 5]
                .pipe(|v| v.into_iter().map(|x| x * 2).collect::<Vec<_>>())
                .pipe(|v| v.into_iter().filter(|x| *x > 4).collect::<Vec<_>>());
    
            assert_eq!(v1, v2);
        }
    }

    Deep Comparison

    OCaml vs Rust: Pipe Operator

    OCaml

    (* Built-in pipe forward operator *)
    let ( |> ) x f = f x
    
    (* Usage *)
    let result = 5 |> double |> add1 |> square  (* 121 *)
    
    (* Multiple types *)
    let result = 42 |> string_of_int |> fun s -> "Result: " ^ s
    

    Rust

    pub trait Pipe: Sized {
        fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B { f(self) }
    }
    impl<T> Pipe for T {}
    
    // Usage
    let result = 5.pipe(double).pipe(add1).pipe(square);  // 121
    
    // Type change
    let result = 42.pipe(to_string).pipe(prefix);
    

    Key Differences

  • OCaml: Built-in |> operator
  • Rust: Extension trait method .pipe()
  • Both: Left-to-right data flow
  • Rust: Also provides pipe_ref and pipe_mut variants
  • Both enable point-free style programming
  • Exercises

  • Pipe with error: Implement pipe_result<T, U, E, F: FnOnce(T) -> Result<U, E>>(self: Result<T, E>, f: F) -> Result<U, E> as a method on Result to chain fallible transformations.
  • Three-stage pipeline: Write a validation pipeline using .pipe that parses a string to integer, multiplies by 2, and formats as "value: N" — all expressed as a left-to-right chain.
  • Compose chain: Use compose to build a single fn(i32) -> String that triples, negates, adds 100, and converts to string, then benchmark it against the equivalent direct call.
  • Open Source Repos