pipei 0.3.0

Generalized `pipe` and `tap` traits for multi-argument chaining and 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 reusable partial application by returning a closure over the remaining arguments. The library similarly provides a multi-argument tap operator for side effects that returns the original value.

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

Installation

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"] }

Basic chaining

pipe passes the value to the function and returns the result. tap passes the value for a side effect—logging, assertions, or mutation—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 curries the first argument of a function, producing 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

tap_with takes a projection that returns an Option; if the result is Some, the side effect runs on the projected value. This bridges the gap when a side effect’s signature does not match the receiver: the projection adapts one to the other, whether by accessing a field, calling .as_ref(), .as_bytes(), or any other transformation. It subsumes the specialized methods from the tap crate (tap_ok, tap_dbg, etc.) using a single generic projection.

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

// Simulating tap's `tap_mut` on a field
(&mut req).tap_with(|r| Some(&mut r.attempts), track_retry)();

// Simulating tap's `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);


// Simulating tap's `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 in pipelines involving Result or Option.

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 to the closure, the closure itself returns a Result. This forces manual Ok wrapping and an extra ? after the pipe call.

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