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
- Readable data transformation chains โ parsing, normalising, and formatting pipelines that read in execution order.
- Ad-hoc function application โ apply any closure to a value inline: `user.pipe(|u| cache.get(u))`.
- Zero cost โ `.pipe(f)` inlines to a direct function call at compile time; no overhead.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Built-in operator | `\ | >` in stdlib | Not 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) | |
| Ownership | GC handles intermediate values | Each step consumes and produces | ||
| Macro option | No | `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); // 22Comparison Table
| Aspect | OCaml | Rust (trait) | Rust (macro) | ||
|---|---|---|---|---|---|
| Syntax | `x \ | > f \ | > g` | `x.pipe(f).pipe(g)` | `pipe!(x => f, g)` |
| Direction | Left-to-right | Left-to-right | Left-to-right | ||
| Operator | Custom infix `\ | >` | Method call | Macro invocation | |
| Generics | Polymorphic `'a` | `T: Sized` bound | Any `Expr` | ||
| Runtime cost | None (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