Can macros be used to define struct methods?


#1

I’m working on a Concurrent FRP library based on Elm, and I’d like to be able to accept variadic arguments to methods on a struct. For instance, I have a lift2 function which applies a function to two sources of data (a “signal” in the Elm jargon) and returns a new source of data

signal.lift2(other_signal, |i, j| -> usize { i + j})

I’d like to be able to define something like this:

signal.liftn(other_signal1, other_signal2, |i, j, k| -> usize { i + j + k })

The ‘ugly’ solution is to nest:

signal.lift2(
    other_signal1.lift2(
        other_signal2, 
        |i, j| -> (usize, usize) { (i, j) },
    ),
    |i, (j, k)| -> usize { i + j + k }
)

I’d really like to be able to keep method-chaining syntax, so I can write execution chains like:

signal
    .lift2(other, |i, j| -> usize { i + j})
    .fold(0, |i, j| -> usize { i + j } )
    .lift(|i| -> bool { i > 0 })

Is there a way to define macros that operate like methods? Ideally

signal.liftn!(a, b, ...n, |i, j, ...n| {})

Thanks for any help!


#2

You can’t have macro invocations in method position, sorry. I know both myself and someone else wrote an RFC about it a while ago, but I don’t believe anything happened on that front.

The best you’re likely to be able to do is write something where the “variable length” part of the call signature is represented by a trait, which you then implement for a fixed set of tuple types. Very quick, very rough, barely tested skeleton:

trait LiftExt: Sized {
    type Head;
    fn liftn<T, F, A, R>(self, rest: T, f: F) -> R
    where T: VarLift<<Self as LiftExt>::Head, Arg=A>, F: FnOnce(A) -> R;
}

trait VarLift<Head> {
    type Arg;
    fn to_arg(self, head: Head) -> <Self as VarLift<Head>>::Arg;
}

impl<Head, T0> VarLift<Head> for (T0,) {
    type Arg = (Head, T0);
    fn to_arg(self, head: Head) -> (Head, T0) {
        (head, self.0)
    }
}

impl<Head, T0, T1> VarLift<Head> for (T0, T1,) {
    type Arg = (Head, T0, T1);
    fn to_arg(self, head: Head) -> (Head, T0, T1) {
        (head, self.0, self.1)
    }
}

// Just for this test:

impl<H> LiftExt for (H,) {
    type Head = H;
    fn liftn<T, F, A, R>(self, rest: T, f: F) -> R
    where T: VarLift<H, Arg=A>, F: FnOnce(A) -> R {
        f(rest.to_arg(self.0))
    }
}

fn main() {
    println!("{}", (0,).liftn((1, 2), |(a, b, c)| a+b+c));
}

Note that all “variadic” things are replaced by tuples since Rust doesn’t really support variadics anywhere other than C FFI and macros.


#3

That’s perfect, thank you so much! Going to take a little while to fully wrap my head around it :wink: