Skip to content

Add a note on iterator exhaustion to the iter module docs#154238

Open
scottmcm wants to merge 4 commits intorust-lang:mainfrom
scottmcm:on-exhaustion
Open

Add a note on iterator exhaustion to the iter module docs#154238
scottmcm wants to merge 4 commits intorust-lang:mainfrom
scottmcm:on-exhaustion

Conversation

@scottmcm
Copy link
Member

Since this keeps coming up (#t-libs > `next` after `None` @ 💬), I wrote up some text about it.

In short,

//! The [`Iterator`] trait *itself* doesn't impose any particular meaning on what will
//! happen if its methods are called again after becoming exhausted.

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.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Mar 23, 2026
@rustbot rustbot added the T-libs Relevant to the library team, which will review and decide on the PR/issue. label Mar 23, 2026
@rustbot
Copy link
Collaborator

rustbot commented Mar 23, 2026

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @scottmcm, libs
  • @scottmcm, libs expanded to 8 candidates

@scottmcm scottmcm added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Mar 23, 2026
//! 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

@scottmcm scottmcm Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@joshtriplett joshtriplett added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Mar 23, 2026
@joshtriplett
Copy link
Member

Nominating, but I personally agree this is not a new API surface.

Co-authored-by: increasing <i@inkreas.ing>
Comment on lines +364 to +383
//! 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.
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly like the wording of this PR. There is only one sentence I object to, I think it is slightly too permissive:

The Iterator trait itself doesn't impose any particular meaning on what will
happen if its methods are called again after becoming exhausted.

Here is my take:

Suggested change
//! 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).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may or may not eventually start returning Some(Item) again at some point.

Copy link
Member Author

@scottmcm scottmcm Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything there is after "may". If you don't do the thing that you "may" do, that doesn't constrain you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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:

  1. "Choose to resume iteration, and […] eventually start returning Some(Item) again at some point."
  2. Don't "choose to resume iteration", do "not eventually start returning Some(Item) again at some point", and instead continue following "Returns None when iteration is finished."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare Future::poll:

Once a future has finished, clients should not poll it again.

Once a future has completed (returned Ready from poll), calling its poll method again may panic, block forever, or cause other kinds of problems; the Future trait 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-libs-api-nominated Nominated for discussion during a libs-api team meeting. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants