Skip to content

Glar35/pipei

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 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.

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

Projection

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

Error Handling

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

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages