๐Ÿฆ€ Functional Rust

053: Pipeline Operator

Difficulty: 1 Level: Beginner Thread a value through a sequence of functions left-to-right โ€” Rust's answer to OCaml's `|>`.

The Problem This Solves

Nested function calls read backwards. `exclaim(shout(trim(" hello ")))` means "first trim, then shout, then exclaim" but your eye reads right-to-left. With three functions it's manageable; with six it's a maze. OCaml's `|>` operator solves this elegantly: `" hello " |> trim |> shout |> exclaim`. Left to right. The data flows in the direction you read. Rust has no `|>` operator. But Rust has method chaining (`.method()`) which achieves the same left-to-right readability for types that own the right methods. When those methods don't exist, a `Pipe` trait and a `pipe!` macro replicate the `|>` pattern for any type.

The Intuition

A pipeline is an assembly line. The product starts at one end, each station transforms it, and the final product comes out the other end. `|>` makes that assembly line visible in code: each `|>` is a station. Rust's `.pipe(f)` does the same thing: `x.pipe(f)` is just `f(x)`. It's reverse function application, nothing more. But the syntax makes the data flow obvious:
5.pipe(double).pipe(add1)   โ†’  reads left to right
add1(double(5))             โ†’  reads right to left

How It Works in Rust

// A trait that adds .pipe(f) to any type
pub trait Pipe: Sized {
 fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B {
     f(self)  // that's it โ€” just call f with self
 }
}
impl<T> Pipe for T {}  // implement for every type

// Macro for explicit visual pipelines
macro_rules! pipe {
 ($val:expr => $($f:expr),+) => {{
     let mut v = $val;
     $(v = $f(v);)+
     v
 }};
}

// Three ways to write the same thing:
let r1 = add1(double(5));           // nested โ€” reads right to left
let r2 = 5.pipe(double).pipe(add1); // trait โ€” reads left to right
let r3 = pipe!(5 => double, add1);  // macro โ€” explicit pipeline syntax
// r1 == r2 == r3 == 11
`FnOnce` (not `Fn`) works here because each step consumes the value and produces a new one. Ownership flows through the chain naturally.

What This Unlocks

Key Differences

ConceptOCamlRust
Built-in operator`\>` in stdlibNot available
Equivalent pattern`x \> f \> g``x.pipe(f).pipe(g)` or `pipe!(x => f, g)`
Method chains`x \> String.uppercase_ascii``x.to_uppercase()` (if method exists)
OwnershipGC handles intermediate valuesEach step consumes and produces
Macro optionNo`pipe!` macro replicates `\>` syntax
//! # Pipeline Operator
//! CS3110 โ€” Chaining transformations left-to-right without nesting.

// ---------------------------------------------------------------------------
// 1. Method chaining (native Rust style)
// ---------------------------------------------------------------------------

/// Double a number.
pub fn double(x: i32) -> i32 {
    2 * x
}

/// Add one to a number.
pub fn add1(x: i32) -> i32 {
    x + 1
}

/// Convert a string to uppercase.
pub fn shout(s: &str) -> String {
    s.to_uppercase()
}

/// Append an exclamation mark.
pub fn exclaim(s: String) -> String {
    s + "!"
}

// ---------------------------------------------------------------------------
// 2. Trait-based pipe (generic, reusable)
// ---------------------------------------------------------------------------

/// Extension trait that adds `.pipe(f)` to any value.
///
/// Equivalent to OCaml's `|>` operator: `x.pipe(f)` is `f(x)`.
pub trait Pipe: Sized {
    fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B {
        f(self)
    }
}

impl<T> Pipe for T {}

// ---------------------------------------------------------------------------
// 3. Macro-based pipe
// ---------------------------------------------------------------------------

/// Chain a value through a sequence of functions left-to-right.
///
/// `pipe!(5 => double, add1)` expands to `add1(double(5))`.
#[macro_export]
macro_rules! pipe {
    ($val:expr => $($f:expr),+ $(,)?) => {{
        let mut v = $val;
        $(v = $f(v);)+
        v
    }};
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    // -- Method chaining (free functions called left-to-right) ---------------

    #[test]
    fn test_numeric_pipeline_functions() {
        // 5 |> double |> add1  โ†’  11
        let result = add1(double(5));
        assert_eq!(result, 11);
    }

    #[test]
    fn test_string_pipeline_functions() {
        // "hello" |> shout |> exclaim  โ†’  "HELLO!"
        let result = exclaim(shout("hello"));
        assert_eq!(result, "HELLO!");
    }

    // -- Trait-based pipe ----------------------------------------------------

    #[test]
    fn test_pipe_trait_numeric() {
        let result = 5.pipe(double).pipe(add1);
        assert_eq!(result, 11);
    }

    #[test]
    fn test_pipe_trait_string() {
        let result = "hello".pipe(shout).pipe(exclaim);
        assert_eq!(result, "HELLO!");
    }

    #[test]
    fn test_pipe_trait_closure() {
        let result = 10.pipe(|x| x * 3).pipe(|x| x - 5).pipe(|x| x.to_string());
        assert_eq!(result, "25");
    }

    // -- Macro pipe ----------------------------------------------------------

    #[test]
    fn test_pipe_macro_numeric() {
        let result = pipe!(5 => double, add1);
        assert_eq!(result, 11);
    }

    #[test]
    fn test_pipe_macro_multi_step() {
        // Matches OCaml: 5 |> double |> add1 |> double  โ†’  22
        let result = pipe!(5 => double, add1, double);
        assert_eq!(result, 22);
    }
}

fn main() {
    println!("{:?}", result, 11);
    println!("{:?}", result, "HELLO!");
    println!("{:?}", result, 11);
}
let ( |> ) x f = f x
let double x = 2 * x
let add1 x = x + 1
let result = 5 |> double |> add1
let shout s = String.uppercase_ascii s
let exclaim s = s ^ "!"
let greeting = "hello" |> shout |> exclaim
let () =
  Printf.printf "%d\n" result;
  Printf.printf "%s\n" greeting

๐Ÿ“Š Detailed Comparison

Comparison: Pipeline Operator

OCaml โ€” custom `|>` operator

๐Ÿช Show OCaml equivalent
let ( |> ) x f = f x
let result  = 5       |> double |> add1    (* 11   *)
let greeting = "hello" |> shout  |> exclaim (* HELLO! *)

Rust โ€” nested calls (equivalent semantics)

let result   = add1(double(5));             // 11
let greeting = exclaim(shout("hello"));     // HELLO!

Rust โ€” trait-based pipe

pub trait Pipe: Sized {
 fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B { f(self) }
}
impl<T> Pipe for T {}

let result   = 5.pipe(double).pipe(add1);
let greeting = "hello".pipe(shout).pipe(exclaim);

Rust โ€” macro pipe

macro_rules! pipe {
 ($val:expr => $($f:expr),+ $(,)?) => {{
     let mut v = $val;
     $(v = $f(v);)+
     v
 }};
}

let result = pipe!(5 => double, add1);           // 11
let chained = pipe!(5 => double, add1, double);  // 22

Comparison Table

AspectOCamlRust (trait)Rust (macro)
Syntax`x \> f \> g``x.pipe(f).pipe(g)``pipe!(x => f, g)`
DirectionLeft-to-rightLeft-to-rightLeft-to-right
OperatorCustom infix `\>`Method callMacro invocation
GenericsPolymorphic `'a``T: Sized` boundAny `Expr`
Runtime costNone (inlined)None (inlined)None (expanded)

Type Signatures

  • OCaml `|>`: `val ( |> ) : 'a -> ('a -> 'b) -> 'b`
  • Rust `Pipe::pipe`: `fn pipe<B, F: FnOnce(Self) -> B>(self, f: F) -> B`

Takeaways

1. `|>` is just reverse application โ€” `x |> f` is `f(x)` โ€” a trivial but powerful idea 2. The `Pipe` trait adds `.pipe()` to every type with a single `impl<T> Pipe for T {}` blanket impl 3. Rust's blanket impl idiom is the idiomatic equivalent of OCaml's operator definition 4. The macro makes multi-step pipelines read like a pipeline: `pipe!(x => f, g, h)` 5. All three Rust approaches compile to identical code โ€” pick based on readability preference