Is it possible to unpack a tuple into function arguments?
Asked Answered
B

4

68

If I want to unpack a tuple and pass it as arguments is there a way to do this:

//Does not compile
fn main() {
    let tuple = (10, Vec::new());
    foo(tuple);
}
fn foo(a: i32, b: Vec<i32>) {
    //Does stuff.
}

Instead of having to do this:

fn main() {
    let tuple = (10, Vec::new());
    foo(tuple.0, tuple.1);
}
fn foo(a: i32, b: Vec<i32>) {
    //Does stuff.
}
Barroom answered 5/10, 2016 at 15:41 Comment(0)
A
46

On a nightly compiler:

#![feature(fn_traits)]

fn main() {
    let tuple = (10, Vec::new());
    std::ops::Fn::call(&foo, tuple);
}
fn foo(a: i32, b: Vec<i32>) {
}

There is AFAIK no stable way to do that.

Afghan answered 5/10, 2016 at 15:57 Comment(3)
But there is, see my answer.Rosannarosanne
@Rosannarosanne I assumed he did not want/could not change the function signature.Afghan
In that case yes, I wouldn't see any other way either.Rosannarosanne
R
48

There is a way, using the magic of pattern matching:

fn main() {
    let tuple = (10, Vec::new());
    foo(tuple);
}

fn foo((a, b): (i32, Vec<i32>)) {
    // do stuff
}

As per Rust reference:

As with let bindings, function arguments are irrefutable patterns, so any pattern that is valid in a let binding is also valid as an argument.

So you can specify an argument like:

(a, b): (i32, Vec<i32>)

just like you would in a let statement.

Rosannarosanne answered 5/10, 2016 at 16:48 Comment(3)
can you elaborate about this a little bit more? Isn't it then like a calling a function with a single argument which happened to be a tuple?Obstruct
@Obstruct Yes, foo is a one argument function that takes a tuple. It just so happens that the tuple is immediately pattern-matched and unpacked in the function parameters.Overview
This proposal does not address the need to "unpack a tuple and pass it as argumentS" (emphasis mine).Infusorian
A
46

On a nightly compiler:

#![feature(fn_traits)]

fn main() {
    let tuple = (10, Vec::new());
    std::ops::Fn::call(&foo, tuple);
}
fn foo(a: i32, b: Vec<i32>) {
}

There is AFAIK no stable way to do that.

Afghan answered 5/10, 2016 at 15:57 Comment(3)
But there is, see my answer.Rosannarosanne
@Rosannarosanne I assumed he did not want/could not change the function signature.Afghan
In that case yes, I wouldn't see any other way either.Rosannarosanne
H
18
let (a, b) = (10, Vec::new());
foo(a, b);
Hyatt answered 5/10, 2016 at 15:45 Comment(1)
I upvoted, but this answer would be better if it explicitly answered the question rather than just offering alternative code.Connection
O
0

This can be done in stable rust with generic functions or traits. Here's an example that uses a trait to pass 2-tuples to functions that accept two parameters.

fn main() {
    let tuple = (0, "hello");
    takes2.call(tuple);
}

fn takes2(a: u8, b: &str) {}

trait Call2<A, B, Z> {
    fn call(self, args: (A, B)) -> Z;
}

impl<F, A, B, Z> Call2<A, B, Z> for F
where
    F: FnOnce(A, B) -> Z,
{
    fn call(self, (a, b): (A, B)) -> Z {
        self(a, b)
    }
}

This is likely overkill in most cases, but if you need to do this kind of thing in a lot of places, it may be worthwhile to use this generic code.

The same idea can be extended to tuples of any size. If you really want to go nuts, you can even use a macro to define similar traits for arbitrary numbers of parameters:

extern crate paste; // FYI

fn main() {
    let pair = (0, "hello");
    let triple = (0, "hello", 1024);
    let quad = (0, "hello", 1024, 3.14);
    takes2.call(pair);
    takes3.call(triple);
    takes4.call(quad);
}

fn takes2(a: u8, b: &str) {}

fn takes3(a: u8, b: &str, c: usize) {}

fn takes4(a: u8, b: &str, c: usize, d: f64) {}

define_tuple_calls!(A, B, C, D);

macro_rules! define_tuple_calls {
    () => {};
    ($A:ident $(, $T:ident)* $(,)?) => {
        paste::paste! {
            trait [<Call $A $($T)*>]<$A, $($T,)* Z> {
                fn call(self, args: ($A, $($T,)*)) -> Z;
            }
            
            impl<F, $A, $($T,)* Z> [<Call $A $($T)*>]<$A, $($T,)* Z> for F
            where
                F: FnOnce($A, $($T,)*) -> Z,
            {
                #[allow(non_snake_case)]
                fn call(self, ($A, $($T,)*): ($A, $($T,)*)) -> Z {
                    self($A, $($T,)*)
                }
            }
        }
        define_tuple_calls!($($T,)*);
    };
}
use define_tuple_calls;

If you'd like to pass a mix of tuples and isolated arguments, as in take4.call((0, "hello"), 1024, 3.14), this starts to get pretty hairy. I can see an approach where you would turn the inputs into a nested tuple, ((0, "hello"), 1024, 3.14), and pass that into the method like take4.call(((0, "hello"), 1024, 3.14)). The call method would need to be more generic. Rather than accepting (A, B, C, D), it would accept impl FlattensTo<(A, B, C, D)>, where FlattensTo<T> is a trait that works like Into<T>, which you need to define and implement for arbitrary combinations of nested tuples or values. To implement this comprehensively without a ton of redundant code, you would likely need to write a procedural macro.

Og answered 1/7, 2023 at 19:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.