New type pattern on Iterator

By convention in rust when a method returns an iterator it returns a struct with the same name of the method.

fn my_iter(&self) -> MyIter { ... }

I cannot recall where I read that but that what is done in the std API.

In my project I have an iterator that is equivalent to

fn iter_with_id(&self) -> impl Iterator<Item = (PackageId, &Package)> {
    self.packages.iter().enumerate().map(|(i, x)| (PackageId(i), x))
}

So to respect convention I create a struct like this (the map part is done in Iterator::next because closure cannot be typed):

pub struct IterWithId<'a>(Enumerate<std::slice::Iter<'a, Package>>);

But iterator involves some marker traits (DoubleEndedIterator, ExactSizeIterator, FusedIterator, TrustedLen) and some optionnal methods (size_hint, count, etc.).
Delegate method call involves a lot of boilerplate code but in other side don't do it may prevents optimisations and/or features.

Is there a crate or a way to automagically implement all special traits for a new typed iterator based on the inner iterator ?
Or am I overthinking and implement only Iterator::next is fine ?

I think it would be ok to not create a new struct, as the type signature makes it clear that you're returning something that implements Iterator (which is probably the most important part). Also, the usage of IterWithId would probably be a bit restricted as it contains a reference back up to the &self, so it might not need to be super documented.

One way to make the signature more clear could be something like like (this would still have the reference to &self problem though):

struct PackageWithId<'a>(PackageId, &Package)

and then return a impl Iterator<item = PackageWithId>.

Then you wouldn't have to implement all the other traits.

Hope this helps!

The standard library returns named iterators where it can because a) it lets you store them in fields[1], and b) returning impl Iterator wasn't around when Rust hit 1.0.

Nowadays, I would just return impl Iterator<Item = ...> and leave it be. The only possible negative is that people won't be able to store your iterator in a struct without also making that struct generic.

That should be fine though, because I would expect the return value from iter_with_id() to be used as a temporary view into your struct and not something that's long-lived. Your IterWithId also doesn't seem to provide extra helper methods (e.g. std::path::Iter has a as_path() method that lets you see the un-consumed parts of the path), so I would see no benefit to explicitly storing a IterWithId field as opposed to some I: Iterator.

`


  1. For example, you might write a parser with 1 token of lookahead like this:

    struct Parser<I: Iterator<Item=Token>> { 
        tokens: std::iter::Peekable<I>,
    }
    
    ↩︎
3 Likes

I mostly agree, I'd just add that I might want to return impl DoubleEndedIterator<Item = …> or impl ExactSizeIterator<Item = …> instead of just Iterator, depending on what it's iterating, so that callers can still have .rev() or .len().

4 Likes

Also, it's worth noting that it's possible to return all of those with impl DoubleEndedIterator<Item = (PackageId, i32)> + ExactSizeIterator.

Yeah, that's a good point.

It really depends on how you intend the iterator to be used and whether you might want to make internal refactorings that will modify the bounds of the return type. For example, HashMap::iter() doesn't implement DoubleEndedIterator whereas the iterator BTreeMap::iter() does, so changing self.packages from a BTreeMap to a HashMap may mean you can't return impl DoubleEndedIterator any more.

Or as the Go proverb so eloquently puts it,

The bigger the interface, the weaker the abstraction.
Rob Pike

Where Iterator gives you just one piece of information (the next() method), DoubleEndedIterator gives you another bit, and returning a named type gives you everything that's publicly accessible.

1 Like

To be fair, I've yet to encounter a real-life scenario where I wanted to switch from BTreeMap to HashMap or vice versa. It just doesn't come up that often because their purposes are different enough in practice. Not to mention that such switching of containers would break other interfaces, too, simply because they require Hash xor Ord, so no matter which of them changes to the other, there will always be a new public trait requirement on their keys.

1 Like

I have; I realized that something needed deterministic iteration order and switched to BTreeMap. This was not a breaking change because the keys were of a fixed type, not a caller-supplied one.

1 Like