The clunkiness of itertools::Either reminds me of the equivalent for Future, futures::future::Either.. Which I used to use a lot until async functions came around. I wonder if this is a case generators will help make more ergonomic..
I expect operations on iterators will still produce iterators, until a terminal operation turns it into something else. If I perform one operation and it becomes something that is no longer an iterator type, it's not very useful for passing around.
Thank you for providing the options. As a beginner, it helps me learn. I think I like the first option the most for the general case, although it requires considerably more typing.
There is no singular iterator type, and I think that's what's tripping you up here?
Iterators in general were added to Rust to make it easy to write code using any iterator, and to have a general interface for iterating over then consuming values. It might help to know that they weren't designed for the sake of producing arbitrary iterators.
I think you're write to draw connections to Futures when talking about -> impl Iterator<...> in particular! The -> impl X feature is most used with Futures, and it -> impl Iterator has the same downside as -> impl Future (and all -> impl Trait usages) that you can only return one concrete type.
The advantage of this model is that we retain maximum efficiency - any code generated using your returned iterator is optimized specifically for its usage, and this lets Rust iterators match and exceed for loops in performance. The disadvantage is that the process of taking multiple different iterators and returning them (or setting a variable to either of them) is very unintuitive.
If you want something like you might see in a GC'd language, you might consider Box<dyn Iterator<Item = T>> which has similar advantages (type erasure) and disadvantages (allocation, virtual calls) to the ones you'll see in many other languages.
But that's exactly what happens. The problem is not that either or both of these aren't iterators; they both are. However, Rust is still statically typed, and iter::empty() has a different type than iter::repeat(…) so you can't mix them like that.
This has nothing to do with iterators per se, and everything to do with the language being statically typed. If you are trying to return two different types from a function, that won't work, regardless of whether they are iterators or something else.
That has nothing to do with the language being statically type (cue Scala), and everything to do with zero-cost abstractions and a lack of proper sum type handling in the language. At the very least Either should be a type in libcore, implementing all standard traits (Iterator, Future, Fn, etc). Ideally there should be language-level support for arbitrary anonymous sum types, so that the OP's example would work with minimal modifications.
Yeah, but it still wouldn't (shouldn't) work as-is, exactly because of static typing. A value (be it a function return value or anything else) has exactly one type, and you can't just assign differently-typed values to it. An enum effectively opts in to a level of dynamism over a closed set of types.
One problem is that it can't -- for example, it can't forward both IntoIterator and Iterator.
Not to mention that there are choices about how to implement the traits. For example, what's the item type for Either<A, B>? Is it Either<A::Item, B::Item>? Or is it just T, requiring A: Iterator<Item = T> & B: Iterator<Item = T>? Both of those are perfectly justifiable.
Both are justifiable, yet one is strictly more general than the other. Either<A, B> should return Either<A::Item, B::Item>, and if A::Item == B::Item == Item, then one could use something like .map(Either::into_inner)to convert into an iterator overItem`.
The issue with Iterator and IntoIterator is indeed unfortunately blocked on specialization. Personally I would prefer to skip the IntoIterator impl since it can be easily side-stepped with an explicit .into_iter() call, but it surely isn't as pretty as desirable.
While this is true from a logical point of view, Item = Either<A::Item, B::Item> still incurs a branch for every item, which might be undesirable in some cases.