Existential iterator vs Concrete iterator struct

Consider the following code(playground):

struct MyIter {}

impl Iterator for MyIter { ... }

fn return_struct() -> MyIter {
    MyIter {}
}

fn return_impl() -> impl Iterator<Item = ()> {
    MyIter {}
}

Which style is preferred?
In std, I see std::vec::IntoIter and std::slice::Iter both being structs that are returned instead of a Iterator trait. One advantage of passing the struct directly is that you are able to access other methods that are not from the iterator trait, like as_slice. For example, I would want to return MyIter instead if I want to be able to call extra_functionality here:

impl MyIter {
    fn extra_functionality(&self) -> () {
        todo!()
    }
}

Are there any other merits of returning a concrete struct? If I don't need extraneous functions for the structs, is it better to return an existential iterator instead, to keep the struct private?

neither style is objectively better than the other. it's all trade-offs, as everything in programming.

one benefit I can think of is it's easier for people to build "wrapper" type for your type. opaque types cannot be named, so if people want to do something on them (not directly calling the trait methods), it's very cumbersome or (maybe even impossible on some occasions). I had so much frustrations trying to create wrapper types for async functions.

it depends, as always. it might be "better" if "better" can be precisely defined. for example, if you have many slightly different ways to iterate your data struct, if you use concrete return types, you have to name all those types, and "naming things is one of the two hardest things in programming"

alert: personal opinion and rant ahead

among the reasons to use impl trait return types, keeping the type private is not a valid argument for me. I always advice against "hiding details", especially for data types. in my opinion, it's more important to make your data structures reusable than, say, your functions or macros or whatever. private types (and private fields in public types) hurts extensibility of your library, and a non-extensible library is not a reusable library.

that being said, I'm ok to use opaque types for simple traits like Iterator or IntoIterator etc.

the most irritating situation is for error handling. I really don't like when a library only return "string-like" errors. (it's not too bad if it's just stringified enums, but it's the worst if all you get is a pretty formatted fancy error trace message. can you imagine you have to use regular expression to extract error information from an Error type returned form a library function?)

3 Likes

The impl Trait feature didn't even exist when Vec and slices were already iterable, so those methods simply couldn't have returned impl Iterator.


You need to know what you want to do with your return value and what you want others to be able to do with it.

If you want to have the flexibility to change the concrete iterator type later, or if it's hard to name, eg. because you are implementing it as a deeply nested chain of iterator adaptors, then impl Iterator is a good solution.

If, however, you already made a separate type for your iterator, with all private state, then you can still change its guts without affecting public API. In this case, you might as well name the concrete type, since that has the added advantage of automatically carrying through information about that type, eg. additional implemented traits such as Clone. People usually tend to forget adding or updating such useful traits in existential position, which can lead to surprising, unintentional API limitations when using impl Trait.

2 Likes

Thank you or your comprehensive comment. I was mainly concerned with the consistency of using MyIter, as in my code I have:

fn func1() -> MyIter { ... }

fn func2() -> impl Iterator<Item = T> {
    func1().map(todo!()) // helper function to map some function on MyIter
}

I suppose it's fine to leave it as it is for now.

right, impl as existential traits seem to be mentioned in this RFC, raised in 2017, while both structs are from 1.0.0, in 2015. I'll mark your answer as the solution, though nerditation's answer also has a helpful warning about the difficulty of handling opaque types.