pipei 0.2.7

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, then returns the original value.

use pipei::TapWith;

struct Server { ip: String, port: u16 }

fn check_ipv4(bytes: &[u8]) {
    assert!(bytes.contains(&b'.'));
}

let s = Server { ip: "127.0.0.1".into(), port: 8080 };

// Alternatively: let s = s.tap_with(...);
(&s).tap_with(|x| x.ip.as_bytes(), check_ipv4)();

assert_eq!(s.port, 8080);

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