Rust: how to assign `iter().map()` or `iter().enumarate()` to same variable

Viewed 67
struct A {...whatever...};
const MY_CONST_USIZE:usize = 127;


// somewhere in function

// vec1_of_A:Vec<A> vec2_of_A_refs:Vec<&A> have values from different data sources and have different inside_item types
let my_iterator;
if my_rand_condition() { // my_rand_condition is random and compiles for sake of simplicity
    my_iterator = vec1_of_A.iter().map(|x| (MY_CONST_USIZE, &x)); // Map<Iter<Vec<A>>>
} else {
    my_iterator = vec2_of_A_refs.iter().enumerate(); // Enumerate<Iter<Vec<&A>>>
}

how to make this code compile?

at the end (based on condition) I would like to have iterator able build from both inputs and I don't know how to integrate these Map and Enumerate types into single variable without calling collect() to materialize iterator as Vec

reading material will be welcomed

2 Answers

In the vec_of_A case, first you need to replace &x with x in your map function. The code you have will never compile because the mapping closure tries to return a reference to one of its parameters, which is never allowed in Rust. To make the types match up, you need to dereference the &&A in the vec2_of_A_refs case to &A instead of trying to add a reference to the other.

Also, -127 is an invalid value for usize, so you need to pick a valid value, or use a different type than usize.

Having fixed those, now you need some type of dynamic dispatch. The simplest approach would be boxing into a Box<dyn Iterator>.

Here is a complete example:

#![allow(unused)]
#![allow(non_snake_case)]

struct A;
// Fixed to be a valid usize.
const MY_CONST_USIZE: usize = usize::MAX;

fn my_rand_condition() -> bool { todo!(); }

fn example() {
    let vec1_of_A: Vec<A> = vec![];
    let vec2_of_A_refs: Vec<&A> = vec![];
    
    let my_iterator: Box<dyn Iterator<Item=(usize, &A)>>;
    if my_rand_condition() {
        // Fixed to return x instead of &x
        my_iterator = Box::new(vec1_of_A.iter().map(|x| (MY_CONST_USIZE, x)));
    } else {
        // Added map to deref &&A to &A to make the types match
        my_iterator = Box::new(vec2_of_A_refs.iter().map(|x| *x).enumerate());
    }
    
    for item in my_iterator {
        // ...
    }
}

(Playground)

Instead of a boxed trait object, you could also use the Either type from the either crate. This is an enum with Left and Right variants, but the Either type itself implements Iterator if both the left and right types also do, with the same type for the Item associated type. For example:

#![allow(unused)]
#![allow(non_snake_case)]

use either::Either;

struct A;
const MY_CONST_USIZE: usize = usize::MAX;

fn my_rand_condition() -> bool { todo!(); }

fn example() {
    let vec1_of_A: Vec<A> = vec![];
    let vec2_of_A_refs: Vec<&A> = vec![];
    
    let my_iterator;
    if my_rand_condition() {
        my_iterator = Either::Left(vec1_of_A.iter().map(|x| (MY_CONST_USIZE, x)));
    } else {
        my_iterator = Either::Right(vec2_of_A_refs.iter().map(|x| *x).enumerate());
    }
    
    for item in my_iterator {
        // ...
    }
}

(Playground)

Why would you choose one approach over the other?

Pros of the Either approach:

  • It does not require a heap allocation to store the iterator.
  • It implements dynamic dispatch via match which is likely (but not guaranteed) to be faster than dynamic dispatch via vtable lookup.

Pros of the boxed trait object approach:

  • It does not depend on any external crates.
  • It scales easily to many different types of iterators; the Either approach quickly becomes unwieldy with more than two types.

You can do this using a Boxed trait object like so:

let my_iterator: Box<dyn Iterator<Item = _>> = if my_rand_condition() {
    Box::new(vec1_of_A.iter().map(|x| (MY_CONST_USIZE, x)))
} else {
    Box::new(vec2_of_A_refs.iter().enumerate().map(|(i, x)| (i, *x)))
};

I don't think this is a good idea generally though. A few things to note:

  • The use of trait objects means the types here must be resolved dynamically. This adds a lot of overhead.
  • The closure in vec1's iterator's map method cannot reference its arguments. Instead the second map must be added to vec2s iterator. The effect of this is that all the items are being copied regardless. If you are doing this, why not collect()? The overhead for creating the Vec or whatever you choose should be less than that of the dynamic resolution.
  • Bit pedantic, but remember if statements are expressions in Rust, and so the assignment can be expressed a little more cleanly as I have done above.
Related