Returned `impl Iterator` loses its `DoubleEndedIterator` capability

This is a kind of "knowledge" issue, not necessary a practical issue. First there is an example code, and then I describe the question. The interesting part is just map combined with rev. Other details do not matter.

fn main() {
    // First iterator in reverse
    let v = vec!["one", "two", "three"];
    for i in v.into_iter().map(|x| x.to_ascii_uppercase()).rev() {
        println!("first: {}", i);
    }

    // Second iterator in reverse
    for i in create_iterator().rev() {
        println!("second: {}", i);
    }
}

fn create_iterator() -> impl Iterator<Item = String> {
    let v = vec!["one", "two", "three"];
    v.into_iter().map(|x| x.to_ascii_uppercase())
}

In the main function there are two for loops which use an iterator. The first loop works, the second doesn't (but that's not my question; read on).

The first loop: If I use map method on a vector iterator I can also add rev method to reverse it because (it seems) map's return type implements DoubleEndedIterator trait.

The second loop: If I have a function that returns an iterator type created by map the double-ended feature is lost if the return type is impl Iterator. I can fix this easily by using return type impl DoubleEndedIterator like below.

fn create_iterator() -> impl DoubleEndedIterator<Item = String> {
    let v = vec!["one", "two", "three"];
    v.into_iter().map(|x| x.to_ascii_uppercase())
}

I would like to understand why the "double-ended" feature is lost? Even if I change the return type to impl DoubleEndedIterator maybe there are (in the future) some other Iterator features which are lost in the process. Is there a generic trait bound like impl AllFeaturesIterator which keeps all features?

Return-position impl Traits (RPITs) are sort of type-erasing the returned value, such that you can only use the functionality of the returned type that is implemented by the traits you specify in your RPIT. From the docs, my emphasis:

Functions can use impl Trait to return an abstract return type. These types stand in for another concrete type where the caller may only use the methods declared by the specified Trait.

That's why, even when the concrete iterator type you are returning is double ended, your users won't be able to use it as such, if it is not reflected in the RPIT.

No. The API stability guarantees of the standard library protect you from Iterator or DoubleEndedIterator ever losing functionality that you depend on.

Given that DoubleEndedIterator is a subtrait of Iterator, you don't need an AllFeaturesIterator trait or type definition to use your returned impl DoubleEndetIterator as both Iterator and DoubleEndedIterator. Otherwise you'd have to define your RPIT as impl Iterator<Item=String> + DoubleEndedIterator<Item=String> to use both interfaces.

5 Likes

return position impl trait is in your type signature, the type checker doesn't expose the hidden type, that's the whole point of using some opaque impl Trait.

if you want the type checker to know the concrete type, then you must spell it out with an explicit named return type.

3 Likes

Thanks. I'm starting to see it as a good thing. Function declaring return type impl Iterator only promises that trait, so function's caller can't get anything more. It's explicit and clear interface.

2 Likes

There is a caveat -- auto traits like Send and Sync are implicitly leaked through impl Trait. This is a good thing when those qualities of your return type depend on some generic parameter, so it can be conditional, but it's a compatibility hazard when some hidden part of your type is the difference between implementing an auto trait or not.

3 Likes

Aside from ergonomic API flexibility for the function writer, the other main point of returning impl Trait is to allow returning unnameable types like closures and compiler-generated futures.

2 Likes