#macro #functional #partial-application

nightly macro partial-application-rs

A macro to transform functions into partially applicable structs

1 unstable release

0.1.0 May 22, 2020

#1091 in Procedural macros

MIT/Apache

24KB
456 lines

Macro to make functions partially applicable

Example

// Simple function
fn add(x: u32, y: u32) -> i64 {
	(x + y) as i64
}

fn main() {
	println!("add: {}",add(1,2));
}

The idea is to define a macro to turn a function into a partial application supporting struct. This would look something like

#[part_app]
fn add(x: u32, y: u32) -> i64 {
	(x + y) as i64
}

fn main() {
	let a = add();
	let two = a.x(|| 2);
    let number = two.y(|| 40);
    assert_eq!(number.call(), 42);
}

The #[part_app] would expand into something like this (edited for brevity).

struct add___Added;

struct add___Empty;

struct __PartialApplication__add_<x, x___FN, y, y___FN, BODYFN>
where
    xFN: FnOnce() -> u32,
    yFN: FnOnce() -> u32,
    BODYFN: FnOnce(u32, u32) -> i64,
{
    xm: ::std::marker::PhantomData<x>,
    ym: ::std::marker::PhantomData<y>,
    x: Option<xFN>,
    y: Option<yFN>,
    body: BODYFN,
}

fn add<x, y>(
) -> __PartialApplication__add_<addEmpty, x, addEmpty, y, impl FnOnce(u32, u32) -> i64>
where
    x: FnOnce() -> u32,
    y: FnOnce() -> u32,
{
    __PartialApplication__add_ {
        x: None,
        y: None,
        xm: ::std::marker::PhantomData,
        ym: ::std::marker::PhantomData,
        body: |x, y| (x + y) as i64,
    }
}

impl<xFN: FnOnce() -> u32, yFN: FnOnce() -> u32, BODYFN: FnOnce(u32, u32) -> i64, y>
    __PartialApplication__add_<addEmpty, xFN, y, yFN, BODYFN>
{
    fn x(
        mut self,
        x: xFN,
    ) -> __PartialApplication__add_<addAdded, xFN, y, yFN, BODYFN> {
        self.x = Some(x);
        unsafe {
            ::std::mem::transmute_copy::<
                __PartialApplication__add_<addEmpty, xFN, y, yFN, BODYFN>,
                __PartialApplication__add_<addAdded, xFN, y, yFN, BODYFN>,
            >(&self)
        }
    }
}

impl<xFN: FnOnce() -> u32, yFN: FnOnce() -> u32, BODYFN: FnOnce(u32, u32) -> i64, x>
    __PartialApplication__add_<x, xFN, addEmpty, yFN, BODYFN>
{
    fn y(
        mut self,
        y: yFN,
    ) -> __PartialApplication__add_<x, xFN, addAdded, yFN, BODYFN> {
        self.y = Some(y);
        unsafe {
            ::std::mem::transmute_copy::<
                __PartialApplication__add_<x, xFN, addEmpty, yFN, BODYFN>,
                __PartialApplication__add_<x, xFN, addAdded, yFN, BODYFN>,
            >(&self)
        }
    }
}

impl<xFN: FnOnce() -> u32, yFN: FnOnce() -> u32, BODYFN: FnOnce(u32, u32) -> i64>
    __PartialApplication__add_<addAdded, xFN, addAdded, yFN, BODYFN>
{
    fn call(self) -> i64 {
        (self.body)(self.x.unwrap()(), self.y.unwrap()())
    }
}

Importantly, this would be a zero-cost abstraction. In theory, the Options will be removed because they are not checked against. The FnOnce can be optimized out (as it's just the compiler manipulating the syntax tree) so then the struct holds no unoptimizable data. This means its size should be effectively 0, and thus it will be optimized away.

How it works

The macro creates a function which produces a builder pattern like struct. The struct is parameterized by which variables are defined. Defining a variable is only implemented for the struct if that variable is not already defined. The final call is defined only when each variable is itself defined. A variable is marked as defined if its place paramater is of type Added. It is marked as undefined when its place is parameterized by type Empty.

Limitations

In an effort to make this as optimizable as possible, I avoid any heap allocations. This prevents me from abstracting over types of closures. Any instance of a PartialApplication struct can only hold one type of closure. This also prevents copying. To avoid this, adding the attribute poly enables heap allocation and thus all closures with the same trait are equally acceptable. The attribute Clone enables partially constructed functions to be cloned before they are called. value enables passing in values instead of structs.

Dependencies

~1.5MB
~36K SLoC