Add a note on iterator exhaustion to the iter module docs#154238
Add a note on iterator exhaustion to the iter module docs#154238scottmcm wants to merge 4 commits intorust-lang:mainfrom
iter module docs#154238Conversation
|
rustbot has assigned @Mark-Simulacrum. Use Why was this reviewer chosen?The reviewer was selected based on:
|
| //! those results away, risks losing items from an `mpsc` channel that the caller | ||
| //! might have wanted to see, for example. Depending what you're doing, it might | ||
| //! be helpful to call [`Iterator::fuse`] on the iterator you're processing so | ||
| //! that you can (vacuously) use it after exhaustion without worry. |
There was a problem hiding this comment.
This has the same consequence of not getting more items from the mpsc try iterator, so it being worded like this fixes it is strange.
That seems like a non-issue anyways
There was a problem hiding this comment.
If you consume the iterator but it was passed as .by_ref() then that's what you want: you don't see the other items but the caller can re-use the iterator to see more.
Imagine code doing let a = [it.next(), it.next(), it.next()]; if a.all(Option::is_some) { Some(a.map(Option::unwrap)) } else { None }, for example.
|
Nominating, but I personally agree this is not a new API surface. |
Co-authored-by: increasing <i@inkreas.ing>
library/core/src/iter/mod.rs
Outdated
| //! The [`Iterator`] trait *itself* doesn't impose any particular meaning on what will | ||
| //! happen if its methods are called again after becoming exhausted. | ||
| //! | ||
| //! Many iterators implement [`FusedIterator`] to convey that once they have reached | ||
| //! exhaustion, they will remain exhausted forever, with `next` returning `None` | ||
| //! for all future calls, `position` returning `None` for all future calls, etc. | ||
| //! The structure of implementing iterators frequently makes this doable for no extra | ||
| //! cost, as a simple consequence of ever returning `None`, so is preferred in those cases. | ||
| //! | ||
| //! Some iterators define what it means to use them again after hitting exhaustion. | ||
| //! For example, [`mpsc::TryIter`] continues to return more items from the channel | ||
| //! (if there are any) when it's called again after exhaustion. Others might find | ||
| //! that their implementation is simplest if they re-iterate the same items again | ||
| //! when called again after exhaustion. | ||
| //! | ||
| //! Occasionally, iterators exist where calling again after exhaustion will misbehave | ||
| //! in some way. That of course must not cause *Undefined Behaviour*, as `Iterator` | ||
| //! is a safe trait. However, if the iterator is non-fused and avoiding doing so | ||
| //! would cause extra cost, a particular iterator might end up panicking in `next` | ||
| //! (or `fold` or `find_map` or ...) if called after exhaustion. |
There was a problem hiding this comment.
I mostly like the wording of this PR. There is only one sentence I object to, I think it is slightly too permissive:
The
Iteratortrait itself doesn't impose any particular meaning on what will
happen if its methods are called again after becoming exhausted.
Here is my take:
| //! The [`Iterator`] trait *itself* doesn't impose any particular meaning on what will | |
| //! happen if its methods are called again after becoming exhausted. | |
| //! | |
| //! Many iterators implement [`FusedIterator`] to convey that once they have reached | |
| //! exhaustion, they will remain exhausted forever, with `next` returning `None` | |
| //! for all future calls, `position` returning `None` for all future calls, etc. | |
| //! The structure of implementing iterators frequently makes this doable for no extra | |
| //! cost, as a simple consequence of ever returning `None`, so is preferred in those cases. | |
| //! | |
| //! Some iterators define what it means to use them again after hitting exhaustion. | |
| //! For example, [`mpsc::TryIter`] continues to return more items from the channel | |
| //! (if there are any) when it's called again after exhaustion. Others might find | |
| //! that their implementation is simplest if they re-iterate the same items again | |
| //! when called again after exhaustion. | |
| //! | |
| //! Occasionally, iterators exist where calling again after exhaustion will misbehave | |
| //! in some way. That of course must not cause *Undefined Behaviour*, as `Iterator` | |
| //! is a safe trait. However, if the iterator is non-fused and avoiding doing so | |
| //! would cause extra cost, a particular iterator might end up panicking in `next` | |
| //! (or `fold` or `find_map` or ...) if called after exhaustion. | |
| //! Calling an iterator's methods again after it becomes exhausted is uncommon, | |
| //! but not in general a misuse of the interface. | |
| //! There are two main options for what an iterator should do | |
| //! if its methods are called again after becoming exhausted: | |
| //! | |
| //! - Many iterators, once they have reached exhaustion, will remain exhausted, | |
| //! with `next` returning `None` for all future calls, | |
| //! `position` returning `None` for all future calls, etc. | |
| //! Iterators of this sort are termed "fused", and should implement [`FusedIterator`] | |
| //! to convey their behavior. | |
| //! | |
| //! - Alternatively, some iterators define what it means | |
| //! to use them again after hitting exhaustion. | |
| //! For example, [`mpsc::TryIter`] continues to return more items from the channel | |
| //! (if there are any) when it's called again after exhaustion. Others might find | |
| //! that their implementation is simplest if they re-iterate the same items again | |
| //! when called again after exhaustion. | |
| //! | |
| //! Occasionally, iterators exist where calling again after exhaustion | |
| //! will misbehave in some way, for example by panicking, | |
| //! because to do anything else would impose an unacceptable cost, | |
| //! e.g. in performance. (That, of course, must not cause *Undefined Behaviour*, | |
| //! as `Iterator`'s methods are safe.) Iterators that must do this | |
| //! should document their behavior prominently | |
| //! (no different from any other potentially-misbehaving API). |
There was a problem hiding this comment.
I'm strongly against this reframing, as it removes the qualitatively most important part to me: that the iterator trait doesn't impose requirements on what happens after exhaustion.
You can choose to document what happens after exhaustion, perhaps in prose, perhaps by implementing FusedIterator, but there's no requirement to do so, and if you don't then there's no reason for a caller to expect anything in particular -- including that it won't panic -- for an iterator that hasn't documented it.
There was a problem hiding this comment.
if you don't then there's no reason for a caller to expect anything in particular -- including that it won't panic -- for an iterator that hasn't documented it.
I don't agree, I don't think this is the proper interpretation of what the documentation has said up to now. Specifically, I think that it is an incorrect reading of this section (relating to next()):
Returns
Nonewhen iteration is finished. Individual iterator implementations may choose to resume iteration, and so callingnext()again may or may not eventually start returningSome(Item)again at some point.
There was a problem hiding this comment.
Everything there is after "may". If you don't do the thing that you "may" do, that doesn't constrain you.
There was a problem hiding this comment.
"Returns None when iteration is finished" is not after "may". My interpretation of that sentence is: whenever next() is called, and iteration is finished, None should be returned.
After that, there is a "may or may not", which sets up a binary choice between two equally valid options:
- "Choose to resume iteration, and […] eventually start returning
Some(Item)again at some point." - Don't "choose to resume iteration", do "not eventually start returning
Some(Item)again at some point", and instead continue following "ReturnsNonewhen iteration is finished."
There was a problem hiding this comment.
That sentence is warning callers to be careful about accidentally dropping Some-after-None items, not giving them license to assume that calling next after None is always going to work.
There was a problem hiding this comment.
Compare Future::poll:
Once a future has finished, clients should not
pollit again.
Once a future has completed (returned
Readyfrompoll), calling itspollmethod again may panic, block forever, or cause other kinds of problems; theFuturetrait places no requirements on the effects of such a call.
When the standard library wants to document that trait implementers don't need to be well-behaved in specific situations, it does so clearly and without ambiguity.
There was a problem hiding this comment.
That's an overreach- Future's documentation being more explicit doesn't imply anything about Iterator (this particular sentence is at least 10 years old, from before Future even existed). The ecosystem already does not abide by this restriction you're reading into the trait anyway- it's clear people have not all been reading this as "returns None forever" but as "returns None once."
Co-authored-by: Josh Triplett <josh@joshtriplett.org>
Since this keeps coming up (#t-libs > `next` after `None` @ 💬), I wrote up some text about it.
In short,
with various things that are possibilities, and some implications thereof.
In my opinion this adds no new requirements on users or implementors of
Iterator, just describes what's de-facto already true: there are various things that concrete iterators do after they're exhausted.There is one thing this describes as "preferred", but that's basically just "if the obvious implementation fuses you should probably be fused", which I hope is non-controversial.