Arguably, it’s kinda annoying – though that doesn’t answer your question here – that the “what happens after None” question even exists in Rust; even though enums (Option in particular) have so nicely solved the problem you see in Iterator-like abstractions in some other languages such as Java where you’ll meet separate methods like hasNext (returning a bool), and then a next method (returning the item type directly) that would need to e.g. throw an exception when called unexpectedly. It’s like… the more capable algebraic types have solved half of the problem (the user can’t force the Iterator to give an item when none is present), but it’s not perfect yet (the iterator can still offer non-None values after it arguably “was done”; but also, the user can still repeatedly ask the iterator in the first place, so to be more “well-behaved”, certain iterators would need to add overhead of sorts).
Now actually talking about your question (and kinda detached from my previous paragraph)…
…what exactly do you mean by “idempotent”? Keep returning the same error again? That would meen needing to Clone the error though, that doesn’t sound ideal… or what did you have in mind? Perhaps that it just ends? Another approach could even be to take inspiration from APIs like Future and to embrace the “if you call this method after we’re done we can just panic” approach – the fused version of the thing may then perhaps offer an additional method to determine the state? IDK maybe that’s making the whole API worse though.
One fun case to think about through such invariants/properties is going to be Zip. I haven’t thought through your particular idea(s), but if one of the iterators has an Err, and you considered Err something “like an item” though [and upstream of a fallible map operation it may actually have been an item], it brings the two halves of the Zip out of sync, which is arguably then just inviting buggy behavior if you don’t think about such cases, right? (For instance, this also makes it rather asymmetric between the two sides.)
Ah, and now I’m noticing the current docs do answer my first question (perhaps?) in that they state it’s asking for consistent Ok(None) after an Err(_) encounter.
And now I got distracted looking at the list of FusedLender impls by an urge to question the validity of claiming fusedness on Cycle
impl<L> FusedLender for Cycle<L>
where
L: Clone + FusedLender,
because arguably, it reasons about the behavior of clones of a lender, even though there aren’t any documented restrictions on their behavior, AFAICT. Though … interestingly, it looks like std’s normal Iterator is even more lax here and even provides:
impl<I> FusedIterator for Cycle<I>
where
I: Clone + Iterator,
with a mere I: Iterator bound. Which would be fine if there was any promise (even on a “logic error” level) that cloning an iterator produces something that has the same values.
Which… can be broken rather easily, e.g. here:
use std::iter::{self, FusedIterator};
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel::<()>();
let it = iter::from_fn(|| rx.try_recv().ok());
let it = it.cycle();
test_fused(it, || _ = tx.send(()));
}
fn test_fused(mut i: impl FusedIterator, cb: impl FnOnce()) {
if i.next().is_none() {
cb();
assert!(i.next().is_none());
}
}
^ this assertion totally fails
(playground)
Edit: Turns out the issue is known (`impl FusedIterator for Cycle<I>` is incorrect · Issue #90085 · rust-lang/rust · GitHub)