pipei allows writing x.pipe(f)(y, z) in place of f(x, y, z), enabling method-style chaining and partial application for multi-argument functions.
It also provides tap for multi-argument side effects that return the original value, tap_proj to compose a projection with a side effect, and tap_cond for conditional execution.
This project is inspired by the UMCS (Unified Method Call Syntax) proposal.
It requires nightly Rust for #![feature(impl_trait_in_assoc_type)].
For general motivation for the tap and pipe operators, see tap.
pipe passes the value as the first argument to a function and returns the result.
tap passes the value to a function for a side effect, then returns the original value.
use pipei::{Pipe, Tap};
fn add(x: i32, y: i32) -> i32 { x + y }
let result = 2
.pipe(add)(3)
.pipe(|x, a, b| a * x + b)(10, 1)
.pipe(Option::Some)();
assert_eq!(result, Some(51));
fn log(x: &i32) { println!("val: {}", x); }
fn add_assign(x: &mut i32, y: i32) { *x += y; }
let val = 2
.tap(log)() // Immutable: inferred &i32
.tap(add_assign)(3); // Mutable: inferred &mut i32
assert_eq!(val, 5);Because pipe returns a closure over the remaining arguments, it doubles as partial application.
use pipei::Pipe;
struct Discount { rate: f64 }
impl Discount {
fn apply(&self, price: f64) -> f64 {
price * (1.0 - self.rate)
}
}
let season_pass = Discount { rate: 0.20 };
// Equivalent to the hypothetical: `let apply_discount = season_pass.apply;`
let apply_discount = season_pass.pipe(Discount::apply);
let prices = [100.0, 200.0, 300.0];
let discounted = prices.map(apply_discount);
assert_eq!(discounted, [80.0, 160.0, 240.0]);tap_proj lets you compose an existing function with a projection on the receiver when the function's signature doesn't match the receiver directly.
tap_cond does the same, but the projection returns Option, allowing for conditional execution; returning None skips the side effect.
Like tap, the original value is always returned.
use pipei::TapWith;
#[derive(Debug)]
struct Request { url: String, attempts: u32 }
fn track_retry(count: &mut u32) { *count += 1 }
fn log_trace<T: core::fmt::Debug>(val: &T, label: &str) { /* ... */ }
let mut req = Request { url: "https://pipei.rs".into(), attempts: 3 };
(&mut req).tap_proj(|r| &mut r.attempts, track_retry)();
assert_eq!(req.attempts, 4);
// tap only on Err
let res = Err::<(), _>(503)
.tap_cond(|x| x.as_ref().err(), log_trace)("request failed");
assert_eq!(res.unwrap_err(), 503);
// tap only in debug builds
let req = req.tap_cond(|r| {
#[cfg(debug_assertions)] { Some(r) }
#[cfg(not(debug_assertions))] { None }
}, log_trace)("FINAL");
assert_eq!(req.attempts, 4);The tap crate provides single-argument pipe and tap traits.
pipei generalizes these to multi-argument functions, so arguments are passed directly rather than nested in closures.
This has the advantage of simplifying fallible pipelines, particularly when using control flow operations.
In the following example, the reading order is inverted ("inside-out"): save is written first, but executes last.
save(
composite_onto(
load("background.png")?,
resize(load("overlay.png")?, 50),
0.8
),
"result.png"
);With the tap crate:
Since ? applies inside the closure, the closure returns a Result, forcing manual Ok wrapping and an extra ?.
load("background.png")?
.pipe(|bg| {
let overlay = load("overlay.png")?
.pipe(|fg| resize(fg, 50));
Ok(composite_onto(bg, overlay, 0.8))
})?
.pipe(|img| save(img, "result.png"));With pipei:
load("background.png")?
.pipe(composite_onto)(
load("overlay.png")?.pipe(resize)(50),
0.8,
)
.pipe(save)("result.png");To optimize compile time, enable only the arities you need (from 0 up to 50).
Use up_to_N features (available in multiples of five) or enable individual arity features.
[dependencies]
pipei = "*" # default: features = ["up_to_10"]
# pipei = { version = "*", features = ["up_to_20", "31"] }
# pipei = { version = "*", features = ["0", "1", "3", "4"] }