How to lazily gather list of things from one-or-many variant enum?

I have this enum,

pub enum IOPath {
    Single(String),
    Dynamic(Vec<String>),
}

I don't care about the variant of the enum.
I just need to gather the single or many strings to a single Vec<&str> depending on the variant of the enum. Like this,

// path: &IOPath
let paths = match path {
  IOPath::Single(p) => {
      vec![p.as_str()]
  }
  IOPath::Dynamic(p) => {
      p.iter().map(|s| s.as_str()).collect() // <-- first iteration
  }
};

for path in paths { // <-- second iteration 
 let path: Path = path.as_ref();
}

Because I am just iterating over paths I don't want to iterate over it twice.

Is there any pattern for further optimization?

How about not collecting into a vector?

use itertools::Either;

pub enum IOPath {
    Single(String),
    Dynamic(Vec<String>),
}

impl IOPath {
    fn iter(&self) -> impl Iterator<Item = &str> {
        match self {
            IOPath::Single(s) => Either::Left(std::iter::once(s.as_str())),
            IOPath::Dynamic(v) => Either::Right(v.iter().map(|s| s.as_str()))
        }
    }
}

Now you can just do

for path in path.iter() {
    ...
}

Ah, I just realized there's a really neat way to do it without itertools:

pub enum IOPath {
    Single(String),
    Dynamic(Vec<String>),
}

impl IOPath {
    fn iter(&self) -> impl Iterator<Item = &str> {
        let slice = match self {
            IOPath::Single(s) => std::slice::from_ref(s),
            IOPath::Dynamic(v) => &v[..],
        };
        slice.iter().map(|s| s.as_str())
    }
}

This uses from_ref to treat a &String as a &[String] of length one.

2 Likes

thanks, alice. I did consider it doing this way.
But I was wondering if there is an obvious way (without using an external crate), that I am missing (being new to rust).

thank you so much, alice. This is the exact thing I was looking for.

By the way, do you think is there any other way of doing it without implementing iter?

I'm not implementing the Iterator trait anywhere. Are you referring to the impl Iterator syntax?

Perhaps you are looking for an as_slice method instead?

No quite. May be my phrasing was confusing. I was wondering if there is any other (easy) way.
But I realize, this is Rust, coming from python, it is a very different world. :grinning:

Well you can also implement IntoIterator and use IOPath directly in the for loop:

impl<'a> IntoIterator for &'a IOPath {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;
    
    fn into_iter(self) -> Self::IntoIter {
        let slice = match self {
            IOPath::Single(s) => std::slice::from_ref(s),
            IOPath::Dynamic(v) => &v[..],
        };
        slice.iter()
    }
}

pub fn iterate(io: IOPath) {
    for path in &io {
        println!("{}", path);
    }
}

playground

Unfortunately this gives an item type of &String instead of &str. To avoid this, you can do this, which is a bit long-winded.

I would probably just go for the iter() method solution.

1 Like

Thank you for so many solutions. I am learning so much.

The reason that you have to manually implement the Iterator trait in my linked solution with item type &str is that the IntoIterator trait requires you to specify the full type of the iterator in the associated type called IntoIter.

In the original solution, I used the Iterator::map method, which has the type of the closure as part of the Map<Self, F> return type (it's the F). In Rust, every single closure has its own unique type, which is what allows the compiler to optimize code using iterators down to the same assembly as the corresponding loop. However, the types of these closures do not have a name that is usable in the source code, and since it is part of the type of Map, this makes it impossible to specify the IntoIter associated type if you are using Map.

To get around this, I reimplemented the Map iterator from the standard library in the MyCustomIterator type, and hardcoded it to call as_str() instead of a user-provided closure. This allows me to give the IntoIter associated type a name. Note that the map used in my custom iterator is not the same as the one I used previously. In this case, it is Option::map.

1 Like

Thank you, alice.

Especially, this part is very informative,

The reason using Iterator::map is fine with the fn iter approach I initially suggested is that in the return type of functions, the impl Trait syntax allows me to say "this returns some type that implements Iterator" without saying which one, sidestepping the no-name issue of using closures.

This feature cannot be used in associated types, however.

1 Like

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.