pipei
pipei provides pipe{i} and tap{i} traits that enable point-free (no closures) function chaining syntax.
They bind the first argument of a free function to the receiver and return a closure for the remaining arguments.
- Pipe: Transforms the input. Returns the result of the function
f. - Tap: Inspects or mutates the input. Ignores the result of
fand returns the original value.
The _with variant project the input (e.g., viewing String as str or Vec<T> as [T]) before passing it to the function.
The _with_mut variant allows side effects on a mutable reference, accepting both mutable functions (like Vec::push) and immutable functions (like len).
Enabling arities
Enable the arities you need via features:
[]
= "0.1" # default: features = ["up_to_5"]
# pipei = { version = "0.1", features = ["up_to_10"] }
# pipei = { version = "0.1", features = ["0", "1", "3", "4"] }
Basic chaining (by value)
pipe passes the value into the function and returns the result. tap moves the value in, runs the function, and returns the original value.
Unified Tap API: tap methods seamlessly accept functions taking either &Self (immutable) or &mut Self (mutable).
use ;
let out = 2
.pipe1 // 2 + 3 = 5
.pipe1 // 5 * 10 = 50
.pipe2; // 50 * 7 + 1 = 351
assert_eq!;
let val = 2
.tap1 // Immutable: passes &i32
.tap1 // Mutable: passes &mut i32
.tap1;
assert_eq!;
Arity 0 (Pipe0 / Tap0)
Pipe0 is useful for passing the receiver to a function that takes only one argument, or for wrapping the receiver in a constructor (like Some or Ok) without extra parentheses.
use ;
let maybe_num = "hello".to_string
.pipe0
.pipe0; // No need for wrapper syntax
assert_eq!;
// Works with both immutable and mutable functions
let s = "hello".to_string
.tap0 // Inspect
.tap0; // Mutate
assert_eq!;
Borrowed views (Projection)
Use _with variants to apply a projection (like Borrow::borrow or AsRef::as_ref) before calling the function. This is useful for type adaptation or component inspection.
use ;
use ;
// 1. Type Adaptation: Project PathBuf -> &Path
let path = from;
path.tap0_with;
// 2. Component Inspection: Validate a field without breaking the flow
let cfg = Config ;
let ready_cfg = cfg.tap0_with;
Mutable views
tap{i}_with_mut allows chaining side effects on a mutable reference. It accepts both mutable and immutable functions.
use ;
let mut v1 = vec!;
v1.pipe1_with_mut;
assert_eq!;
let mut v2 = vec!;
v2.tap0_with_mut // Immutable Fn(&Vec) works on mutable view
.tap1_with_mut; // Mutable Fn(&mut Vec) works
assert_eq!;
Comparison with the tap crate
The tap crate is the standard solution for continuous chaining.
pipei extends this concept to multi-argument functions to address issues related to control flow and nesting depth.
1. Control Flow (The ? Operator)
Using closures prevents the ? operator from propagating errors out of the parent function.
Standard Rust: We are forced to use intermediate variables to handle the ? operator at each step.
Using tap:
The ? operator affects the closure's return type, not the function's.
This leads to compilation errors or type mismatches because the closure returns a Result, but the chain expects the unwrapped value.
Using pipei:
Arguments are evaluated before the call, so the ? operator works exactly as intended. It allows you to build a clean pipeline even with fallible free functions.
2. Recursive Nesting
When function arguments are results of other chains, standard chaining forces deep closure nesting. pipei maintains a flat structure. To illustrate this, consider the following example:
Standard Rust:
The logic is "inside-out": save is written first, but happens last. The "background" (our starting point) is buried in the middle.
save;
Using tap: We restore the top-to-bottom flow, but processing the second argument (the overlay) requires opening a nested closure, adding visual clutter and possibly making any control flow problem even more severe.
load.pipe.pipe;
Using pipei:
The primary flow (load -> blend -> save) remains linear. The secondary flow (overlay -> resize) is handled inline without closures, keeping the code clean and declarative.
load
.pipe2
.pipe1;