How to iterate over a tuple or variables with different types?

I have the following code:

use std::any::type_name_of_val;
fn main() {
    let (a, b, c, d) = (0, 0., '0', "0"); // something many variables
    println!("{}", type_name_of_val(&a));
    println!("{}", type_name_of_val(&b));
    println!("{}", type_name_of_val(&c));
    println!("{}", type_name_of_val(&d));
}

I want to use for and iterate over a tuple (a, b, c, d) and print the type of the value. I searched, and found rust - Is it possible to iterate over a tuple? - Stack Overflow but it only says tuple can't be iterated and iterate over a same type. I also found Reddit - Dive into anything but there is no solution. How to iterate over a multiple variables with different types in Rust? C++ version is like this:

#include <tuple>
#include <iostream>
#include <boost/core/demangle.hpp>

int main() {
    auto const [a, b, c, d] = std::tuple{0, 0., '0', "0"};
    std::apply([](auto&&... args) {
        ((std::cout << boost::core::demangle(typeid(args).name()) << std::endl), ...);
    }, std::tuple{a, b, c, d});
}

Regardless of what you're iterating over, all the iteration items must by definition have the same type.
So what you want isn't directly possible.

What you could do is wrap them in an enum that has a variant for each type you want to iterate over, and then iterate over the wrapped values.

Which brings us to the 2nd part, iterating over tuples. That's not possible, because tuples in general have fields with pretty much arbitrary types. As stated above, iterating over with multiple types is not possible.

3 Likes

Rust is strictly and statically typed, e.g. every monomorphized expression has the same type. And Rust currently doesn't have generic variadics (with tuple flattening) or the like either. Or const based tuple indexing, even. So any solution is probably going to be

Brute forcing some way forward with tuples and a boatload of (probably macro generated) boilerplate isn't unheard of, but it's not common or idiomatic either. In typical code, you just don't do that.

3 Likes

jjpe: What you could do is wrap them in an enum that has a variant for each type you want to iterate over, and then iterate over the wrapped values.

quinedot: altering your types (e.g. use an enum and array instead)

Can you give me some examples?

enum Dynamic {
    Int(i64),
    Float(f64),
    Char(char),
    Str(&'static str),
}

let arr = [
    Dynamic::Int(0),
    Dynamic::Float(0.0),
    Dynamic::Char('0'),
    Dynamic::Str("0"),
];

for item in arr {
    let type_name = match item {
        Dynamic::Int(_) => "integer",
        Dynamic::Float(_) => "floating-point",
        Dynamic::Char(_) => "character",
        Dynamic::Str(_) => "string",
    };
    println!("{type_name}");
}

It would also help to know why you want this. People are sometimes too fast to recommend enums when a better solution would be generics or dynamic dispatch. What's your use case?

4 Likes

If we get $n$ more variables or values, the code increases by $3n$ lines. And the code need to specify the type of literal in advance to know or print the type of literal. So enums way seems not good for my purpose.

That's normal for Rust.

Which is, of course, true for any other solution.

But what is that mysterious purpose?

Sounds like an attempt to write some other laguage in Rust. Don't do that.

Or, more likely, an XY Problem.

This being said, it would be hypocrisy for me to say that I have never iterated over tuples. I did. Took around 2000 lines of code to iterate over one. This allowed me to go from 100000 [autogenerated] functions (no typo!) to around 2000 [autogenerated] functions thus was, most definitely, worth it.

But depending of what do you plan to do all that complexity may or may not be worth it.

1 Like

If you want to support an open set of arbitrary types in a collection, then you'll need dynamic dispatch. For example:

trait NamedType {
    fn type_name(&self) -> &'static str;
}

impl<T: ?Sized> NamedType for T {
    fn type_name(&self) -> &'static str {
        core::any::type_name::<Self>()
    }
}

let arr: &[&dyn NamedType] = &[
    &0,
    &0.0,
    &'0',
    &"0",
];

for &val in arr {
    println!("{}", val.type_name());
}
4 Likes

I am comparing Rust with other languages. One of my purposes is to check the function (or system, not the mathematical mapping) of the tuple of Rust. Another purpose is to show the literal and the corresponding types in Rust in a comparison text.

For showing the literal and type correspondence, macro seems good for my purpose.

macro_rules! println_types {
	($($x:expr),*) => {
		$(println!("{}", std::any::type_name_of_val($x));)*
	};
}

fn main() {
	let (a, b, c, d) = (0, 0., '0', "0");
	println_types!(&a, &b, &c, &d);
}

The ideal solution very much depends on what you want to do with the values inside the loop - other than printing them and their type. The code you have may be simple, but it's a little unrealistic to be your real goal. Rust may not be the best language to write "example code" in, as it sometimes forces you to be hyper correct.

For example, if you want to calculate a hash of your items, you could use a Vec<&dyn Hash> or something.

1 Like

You can also use generics (although, due to the lack of variadic tuples, the impl will require a macro, too): Playground

1 Like

In that case best way would be to start with C++98. Can you show us how to deal with what you plan to do starting from C++98 tuple:

#include <string>
#include <tr1/tuple>

int main() {
    std::tr1::tuple<int, double, char, std::string> my_tuple(0, 0., '0', "0");
}

Main limitation that Rust have that is related to dealing with tuples is lack of variadics. That's also the issue with C++98 thus to do what you want to do you would have to adopt C++98 solution, not C++17 one.

3 Likes

The loop can be modeled by a function iterate, implemented separately for each length, with the loop body as a callback. However, since a function parameterized by types cannot be given as an argument to a function, we split the callback function via defunctionalization into a part erase, which maps the different element types of the tuple to a fixed type, and cb, which constitutes the remaining loop body.

trait Erase<X> {
    fn apply<T>(&self, x: &T) -> X;
}

trait IterTuple<X, F: Erase<X>> {
    fn iterate(self, erase: F, cb: impl FnMut(X));    
}

impl<T0, X, F: Erase<X>> IterTuple<X, F> for (T0,) {
    fn iterate(self, erase: F, mut cb: impl FnMut(X)) {
        cb(erase.apply(&self.0));
    }
}
impl<T0, T1, X, F: Erase<X>> IterTuple<X, F> for (T0, T1) {
    fn iterate(self, erase: F, mut cb: impl FnMut(X)) {
        cb(erase.apply(&self.0));
        cb(erase.apply(&self.1));
    }
}
impl<T0, T1, T2, X, F: Erase<X>> IterTuple<X, F> for (T0, T1, T2) {
    fn iterate(self, erase: F, mut cb: impl FnMut(X)) {
        cb(erase.apply(&self.0));
        cb(erase.apply(&self.1));
        cb(erase.apply(&self.2));
    }
}
impl<T0, T1, T2, T3, X, F: Erase<X>> IterTuple<X, F> for (T0, T1, T2, T3) {
    fn iterate(self, erase: F, mut cb: impl FnMut(X)) {
        cb(erase.apply(&self.0));
        cb(erase.apply(&self.1));
        cb(erase.apply(&self.2));
        cb(erase.apply(&self.3));
    }
}

fn main() {
    use std::any::type_name_of_val;
    struct TypeName;
    impl Erase<&'static str> for TypeName {
        fn apply<T>(&self, x: &T) -> &'static str {type_name_of_val(x)}
    }

    let t = (0, 0.0, '0', "0");
    t.iterate(TypeName, |x| {
        println!("{}", x);
    });
}

A literal function can't, but that's just due to the lack of syntax. My solution above does exactly this, except with a custom trait instead of Fn, and an explicitly named callback type in place of an unnameable closure literal or fn item type.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.