pipei 0.2.9

Generalized pipe and tap for multi-argument function chaining.
Documentation

pipe{i}

pipei provides a zero-cost, type-safe way to chain multi-argument functions using method syntax. It turns a function call f(x, y, z) into a method call x.pipe(f)(y, z). It also includes a tap operator for side-effects (logging, mutation) that returns the original value.

This project is inspired by the UMCS proposal. It generalizes the tap crate to support multi-argument pipelines.

Note: Requires #![feature(impl_trait_in_assoc_type)] on nightly.

To optimize compile times, enable only the arities you need (from 0 up to 50). Use features like up_to_N (where N is a multiple of 5) or specific individual arity features

[dependencies]
pipei = "*" # default: features = ["up_to_5"]
# pipei = { version = "*", features = ["up_to_20"] }  
# pipei = { version = "*", features = ["0", "1", "3", "4"] }

Basic chaining

pipe passes the value into the function and returns the result. tap inspects or mutates the input, ignores the result, and returns the original value.

use pipei::{Pipe, Tap};

fn add(x: i32, y: i32) -> i32 { x + y }
fn mul(x: i32, y: i32) -> i32 { x * y }
fn lin(x: i32, a: i32, b: i32) -> i32 { a * x + b }

let maybe_num = 2
    .pipe(add)(3)      
    .pipe(mul)(10)    
    .pipe(lin)(7, 1)
    .pipe(Option::Some)();

assert_eq!(maybe_num, Some(351));

fn log_val(x: &i32) { println!("val: {}", x); }
fn add_assign(x: &mut i32, y: i32) { *x += y; }

let val = 2
    .tap(log_val)()         // Immutable: passes &i32
    .tap(add_assign)(3)     // Mutable: passes &mut i32

assert_eq!(val, 5);

Partial Application

pipe can pre-fill the first argument of a function, creating a standalone, reusable function that accepts the remaining arguments.

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

TapWith

Runs a side-effect on a projection of the value, which then returns the original value. This is useful for reusing existing functions on a derived value (such as a field). The side-effect executes only if the projection returns Some, which enables conditional flows (like tap_ok) and debug-only operations.

use pipei::TapWith;

#[derive(Debug)]
struct Request { url: String, attempts: u32 }

fn track_retry(count: &mut u32) { *count += 1 }
fn log_status(code: &u32, count: u32) { /* ... */ }
fn log_trace(req: &Request, label: &str) { /* ... */ }

let mut req = Request { url: "https://api.rs".into(), attempts: 3 };

// tap_mut on a field
(&mut req).tap_with(|r| Some(&mut r.attempts), track_retry)();

// tap_err (only tap on error)
let res = Err::<Request, _>(503)
    .tap_with(|x| x.as_ref().err(), log_status)(req.attempts);

assert_eq!(res.unwrap_err(), 503);


// tap_dbg (only tap in debug mode)
let final_req = req.tap_with(|r| {
    #[cfg(debug_assertions)] { Some(r) }
    #[cfg(not(debug_assertions))] { None }
    }, log_trace)("FINAL_STATE");


assert_eq!(final_req.attempts, 4);

Comparison with the tap crate

pipei generalizes tap to support multi-argument functions, reducing syntactic noise and simplifying control flow when pipelines involve Result or Option types.

Standard Rust: The reading order is inverted ("inside-out"), as 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 to the closure, the closure itself returns a Result. This forces manual Ok wrapping and an extra ? after the pipe.

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: The flow remains flat and ? works naturally.

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