pipei
pipei provides pipe{i} and tap{i} traits that enable point-free 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 variants allow projecting the input (e.g., viewing String as str or Vec<T> as [T]) before passing it to the function.
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 = ["pipe0", "pipe1", "tap0", "tap1"] }
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 ;
assert_eq!;
// Wraps value in Option::Some (removing wrapper syntax)
let maybe_num = 10.pipe0;
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.
use ;
use Borrow;
let s = "hello world".to_string;
// |x| x.borrow() projects &String -> &str
assert_eq!;
s.tap2_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 continues 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:
Using tap:
The ? operator affects the closure's return type, not the function's, which may lead to type mismatches which may be hard to resolve (especially when used recursively).
Using pipei:
Arguments are evaluated before the call, so the ? operator works as usual.
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 this (admittedly contrived) example:
In Standard form, the logic is "inside-out".
let result = process_one;
By using tap we have recursive nested calls with recursive nested causers:
let result = x_very_long_name
.pipe;
Using pipei, the logic remains linear.
let result = x_very_long_name
.pipe2;