I wish Rust was trait-aware when arm-matching

Simple example:

  let some_vec = match self.lang {
   Lang::A => string_string_map.keys(),
   Lang::B => string_string_map.values(),
  }.collect::<Vec<_>>();

This will never work (in Rust 1), right?

Rust requires each arm in a match to have the same type. Therefore, a solution would be to move the .collect() into each match-arm so both return Vec<_>. Another solution would be to put each Iterator into a Box<dyn Iterator<...>> and then collect it.

@farnbams this is more about where Rust can reach abstraction-wise in the future.


Sidenote: Adding a category to the forum that communicates "not-help" would be useful, I think.

In order to give consistent semantics for match, ie. let it work in itself too, not only in the context of whatever method calls you perform on its result, it has to be able to unify the types of its arms. It doesn't make sense to accept completely different types even if they share the same trait (it's also very difficult and problematic in terms of eg. memory layout).

If the types share a common trait, you can coerce each arm into a trait object (eg. &dny mut Iterator or Box<dyn Iterator>) and then use the resulting trait object to .collect().

It's not exactly pretty but here's an example

let iter = match some_vec {
        Some(v) => {
            Box::new(
            v.into_iter()
                .map(|i: usize| Some(i))
                .filter_map(|i| i)
            ) as Box<dyn Iterator<Item=usize>>
        },
        None => {
            Box::new(str_vec.iter().map(|&ch| {
                let num: usize = ch.parse().expect("");
                num
            }))
        },
    };

then do whatever with the resulting iterator as 'Carbonic Acid' said

playground

1 Like

Are you trying to write something like this?

let vec: Vec<String> = match lang {
    Lang::A => Box::new(map.keys()) as Box<dyn Iterator<Item=&String>>,
    Lang::B => Box::new(map.values()) as Box<dyn Iterator<Item=&String>>,
}.cloned().collect();

To remove the type duplication you can write it likes this (generalized type ascriptions may also help in some situations):

let iter: Box<dyn Iterator<Item=&String>> =  match lang {
    Lang::A => Box::new(map.keys()) as _,
    Lang::B => Box::new(map.values()) as _,
};
let vec: Vec<String> = iter.cloned().collect();

Doing implicit casts or making significant transformations of the code flow (i.e. moving collect() into match arms) will be heavily against Rust values in my opinion.

BTW I think it should be possible to write a macro which will look roughly like this:

let some_vec: Vec<String> = foo!(
    tmp, // temp variable which will store arm result
    match self.lang {
        Lang::A => string_string_map.keys(),
        Lang::B => string_string_map.values(),
    }, // match arms
   { tmp.cloned().collect() }, // which will use match result.
);
// it will be desugared into
let some_vec: Vec<String> = match self.lang {
    Lang::A => {
        let tmp = string_string_map.keys();
        tmp.cloned().collect()
    },
    Lang::B => {
        let tmp = string_string_map.values();
        tmp.cloned().collect()
    },
};
2 Likes

Another alternative would be to unify the two types into one using an enum that implements Iterator. I'd personally prefer this to the Box<dyn Iterator> approach, because it avoids dynamic dispatch (although admittedly that's probably premature optimization).

The main downside to that approach is the amount of boilerplate, but the either crate makes it very simple:

let some_vec = match self.lang {
    Lang::A => Either::Left(string_string_map.keys()),
    Lang::B => Either::Right(string_string_map.values()),
}.collect::<Vec<_>>();
7 Likes

(In fact, this is very similar, if not identical, to the problem of impl trait-returning functions with multiple concrete return types within the function body.)

2 Likes

As I already mentioned, this is not a help thread. I wasn't stuck or anything. But yes, I agree this is probably the best way to go about this today. So, I marked it as a solution.

1 Like