[Idea] Automatic conversion of tuples as function arguments

Let’s have function

fn foo(x: i32, y: i32) -> i32 { x + y }

From the mathematical point of view, a function which takes two arguments is basically the same as a function which takes a two-tuple, because we can easily transform them either way without losing information.

fn foo2((x, y): (i32, i32)) -> i32 { x + y }

So what about allowing automatic destructuring here:

foo2(7, 15);
let x = (7, 15);
foo(x);

Is there any reason why Rust doesn’t support it other than nobody has implemented it?

For simple functions this might seem obvious, but once you get into generic functions or functions taking a tuple argument + more arguments this is less clear imo.

How many ways should I be able to call the following function?

fn foo2((x, y): (i32, i32), z: i32) -> i32 { x + y + z }

Maybe once we get variadic generics we could implement something like (5, 6).map(foo), where all tuples implement map for FnOnce(arg_num_is_tuple_len) -> T.

Perhaps the name "apply" is more correct than "map". But while I agree I'd like a way to map/apply a tuple like that, I am not sure doing it automatically is a clean design. Unclean (or implicit) designs cause problems later.

In code like this:

fn main() {
    fn mul2(x: u32, y: u32) -> u32 { x * y }
    let a1 = [1, 2, 3];
    let a2 = [10, 20, 30];
    let p: u32 = a1.iter().zip(&a2).map(|(&x, &y)| mul2(x, y)).sum();
    println!("{}", p);
}

You wish to write:

let p: u32 = a1.iter().zip(&a2).map(|t| t.apply(mul2)).sum();

Is this worth having? I am not sure...

The swift devs have spent the last couple years trying to backpeddle on implementing “tuple splatting” because it turns out to be a bit of a nightmare in the details.

I’d rather not repeat their mistakes here.

12 Likes

I think the direction here, to avoid the magic, is something like variadic generics, so the example becomes

let x = (7, 15);
foo(...x);

Where it’s still easy to call a function with the values of the tuple, but it’s unambiguous.

7 Likes

This is a good point and I somewhat agree with what @scottmcm suggested. Maybe there could be a macro, which converts a tuple to a list of arguments? Currently it’s impossible since macros have to return a single object.

A macro alone wouldn’t help, since it cannot inspect the type of an arbitrary tuple x to know its size. Depending on what you’re trying to do with it, you can probably get the brunt of the work done by writing a trait that basically resembles Fn{,Once,Mut}.

An extension trait can do this (playground)

fn main() {
    let x = (1, 2);
    let y = std::cmp::min.call_with_tuple(x);
    println!("{}", y);

    let z = i32::default.call_with_tuple(());
    println!("{}", z);
}

trait Ext<T> {
    type Output;
    fn call_with_tuple(self, x: T) -> Self::Output;
}

impl<T, TR> Ext<()> for T
where
    T: FnOnce() -> TR,
{
    type Output = TR;
    fn call_with_tuple(self, _: ()) -> Self::Output {
        self()
    }
}

impl<T, T0, T1, TR> Ext<(T0, T1)> for T
where
    T: FnOnce(T0, T1) -> TR,
{
    type Output = TR;
    fn call_with_tuple(self, x: (T0, T1)) -> Self::Output {
        self(x.0, x.1)
    }
}

This feature exists and is really useful in Python, but Python is a dynamic interpreted language and it has a much richer function-call syntax. I am not sure it would be useful in Rust. Also, we don’t tend to do things just for the mathematical elegance of it in Rust. You must have something you want to use it for or you wouldn’t have brought it up, so would you mind posting some concrete examples where you think it would make the overall code clearer?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.