Retruning parametric iterator on an inner struct

So, it's quite common in Python to pass around generators. Like

class MyClass:
    def give_something(self):
        # this is like returning iterator in Rust
        return (i * 2 for i in self.data)


myobj = MyClass(some_data)
for item in myobj.give_something():
    print(item)

Unfortunately, in Rust I banged my head against lifetime issues and had to return Vecs, paying the allocation costs.

Finally, my brain figured this out: lifetime should be on the returned iterator generic type, AND inside the map/filter callbacks!

    pub fn find_derivatives<'a>(&'a self, key: &'a Key) -> impl Iterator<Item = Val> + 'a {
                 // in my real-life case, &Key actually didn't need 'a
        self.data.iter()
             // (I'm surprised it's && here, not &)
            .filter(|(k, _): &&'a (Key, Val)| *k == *key)
            .map(|(_, v): &'a (Key, Val)| Val(v.0 * 2))
    }
}

Playground

Of course, if self.data is public, you can just use it directly, but this means tight coupling, harder to refactor, and so on.

I'm a bit surprised it's not in some books or tutorials. Or have I missed something?

This also works:

    pub fn find_derivatives<'a>(&'a self, key: &'a Key) -> impl Iterator<Item = Val> + 'a {
        self.data.iter()
            .filter(move |(k, _)| k == key)
            .map(|(_, v)| Val(v.0 * 2))
    }

There's a double reference here because filter is generic and designed to work with any Item type— It always adds a level of reference so that the item can be later yielded from the iterator if it passes the filter. When the iterator item type is itself a reference, this results in two &s.

3 Likes

The official books and documentation lags a bit, sadly. Also there are some rough edges about how impl Trait captures, or doesn't capture, lifetimes. And how that happens is going to change in the next edition to boot.

For example, if this was in a trait, the impl Trait would have silently captured your input lifetimes, and you could have just written

pub fn find_derivatives(&self, key: &Key) -> impl Iterator<Item = Val> {

And the same will be true outside of traits in the next edition.

The downsides are that it's much less apparent that the return value is still holding a borrow of all the inputs, and there will be cases of overcapture (holding on to borrows you don't need to) instead of undercapture (not allowing holding on to borrows you need to).

The plan is to also have some new syntax for precisely indicating what is captured.


I have a very short blurb here, and one could probably get from the last example to your solution. But what we really deserve is a writeup of all the RPIT-like language features (including the planned type alias).

(n.b. + 'a is perfectly fine for your use case; the main downside is that it imposes an outlives bound on everything else, including captured type generics, but those downsides don't apply to your code.)

2 Likes

Yes, it's just this playground example that I had to add 'a to the input. I have stepped on that rake too, half a year ago -- removing explicit lifetime fixed an issue, and it was quite an epiphany, and probably gave more gut feeling of how lifetimes work.

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.