pipei 0.3.2

Multi-argument pipe and tap with partial application.
Documentation

pipe{i}

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 and tap_with for multi-argument side effects that return the original value.

This project is inspired by the UMCS (Universal Method Call Syntax) proposal. It requires nightly Rust for #![feature(impl_trait_in_assoc_type)].

Basic Chaining

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);

Partial Application

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]);

Projected Side Effects

tap_with lets you reuse a side-effect function whose signature doesn't match the receiver. A projection extracts the relevant part; if it returns Some, the side effect runs on the projected value. If None, it is skipped. In both cases, the original value is returned.

# use pipei::TapWith;
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 };

// track_retry expects &mut u32, not &mut Request — the projection bridges the gap
(&mut req).tap_with(|r| Some(&mut r.attempts), track_retry)();
assert_eq!(req.attempts, 4);

// tap only on Err
let res = Err::<(), _>(503)
    .tap_with(|x| x.as_ref().err(), log_trace)("request failed");
assert_eq!(res.unwrap_err(), 503);

// tap only in debug builds
let req = req.tap_with(|r| {
    #[cfg(debug_assertions)] { Some(r) }
    #[cfg(not(debug_assertions))] { None }
    }, log_trace)("FINAL");
assert_eq!(req.attempts, 4);

Error Handling

The tap crate provides single-argument pipe and tap traits. pipei generalizes both to multi-argument functions, which particularly simplifies fallible pipelines where ? interacts poorly with closures.

Standard Rust: 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"                            
);

Using tap 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"));

Using pipei:

load("background.png")?
    .pipe(composite_onto)(
        load("overlay.png")?.pipe(resize)(50), 
        0.8,
    )
    .pipe(save)("result.png");

Feature Flags

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_5"]
# pipei = { version = "*", features = ["up_to_20", "31"] }  
# pipei = { version = "*", features = ["0", "1", "3", "4"] }