Skip to content

Default implementation on std::iter::Fuse should not requires Default on the inner iterator #140961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ultimaweapon opened this issue May 13, 2025 · 4 comments
Labels
A-iterators Area: Iterators C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@ultimaweapon
Copy link

So it can be constructed in the following code:

let items = match std::fs::read_dir(&path) {
    Ok(v) => v.fuse(),
    Err(e) if e.kind() == ErrorKind::NotFound => Default::default(), // This line won't compile.
    Err(e) => return Err(e),
};
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label May 13, 2025
@ShE3py
Copy link
Contributor

ShE3py commented May 13, 2025

The problem is that Fuse::<I>::default() calls I::default(), which is free to yield some elements. Maybe we should add a Fuse::<I>::exhausted() which is guaranteed to yield no elements?

@rustbot label +T-libs-api +C-feature-request +A-iterators -needs-triage

@rustbot rustbot added A-iterators Area: Iterators C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels May 13, 2025
@hanna-kruppe
Copy link
Contributor

Oh no… that’s what the documentation of impl Default for Fuse says, but the current implementation actually constructs Fuse { iter: Default::default() } which is Fuse { iter: None } because the field is Option<I>.

@zachs18
Copy link
Contributor

zachs18 commented May 14, 2025

For a workaround for the original code snippet, assuming you want an empty iterator in the ErrorKind::NotFound case, there are a few things you could do.

If you don't mind an additional heap allocation and dynamic dispatch, you could use Box<dyn Iterator>:

let mut items: Box<dyn Iterator<Item = _>> = match std::fs::read_dir(&path) {
    Ok(v) => Box::new(v),
    Err(e) if e.kind() == ErrorKind::NotFound => Box::new(std::iter::empty()),
    Err(e) => return Err(e),
};

Or you could use Option::into_iter and Iterator::flatten, e.g.

let mut items = match std::fs::read_dir(&path) {
    Ok(v) => Some(v).into_iter().flatten(),
    Err(e) if e.kind() == ErrorKind::NotFound => None.into_iter().flatten(),
    Err(e) => return Err(e),
};

Or you could use either::Either:

let mut items = match std::fs::read_dir(&path) {
    Ok(v) => Either::Left(v),
    Err(e) if e.kind() == ErrorKind::NotFound => Either::Right(std::iter::empty()),
    Err(e) => return Err(e),
};

Or, if you are using Rust 1.79.0 or later and depending on how you use items, you could use dynamic dispatch with the new temporary value lifetime extension semantics in match:

let items: &mut dyn Iterator<Item = _> = match std::fs::read_dir(&path) {
    Ok(v) => &mut {v}, // the braces are required, to move `v` out of the variable into a temporary whose lifetime can be extended
    Err(e) if e.kind() == ErrorKind::NotFound => &mut std::iter::empty(),
    Err(e) => return Err(e),
};

@ultimaweapon
Copy link
Author

Thanks for the idea. I don't mind using a workaround but I think it is better to be able to construct Fuse directly is better (e.g. provide additional method like @ShE3py said). Also I think changing the current behavior of Fuse::default is a breaking change since people might already depend on its current behavior.

bors added a commit to rust-lang-ci/rust that referenced this issue May 15, 2025
Change `core::iter::Fuse`'s `Default` impl to do what its docs say it does

The [docs on `impl<I: Default> Default for core::iter::Fuse<I>`](https://doc.rust-lang.org/nightly/std/iter/struct.Fuse.html#impl-Default-for-Fuse%3CI%3E) say (as the `I: Default` bound implies) that `Fuse::<I>::default` "Creates a `Fuse` iterator from the default value of `I`". However, the implementation creates a `Fuse` with `Fuse { iter: Default::default() }`, and since the `iter` field is an `Option<I>`, this is actually `Fuse { iter: None }`, not `Fuse { iter: Some(I::default()) }`, so `Fuse::<I>::default()` always returns an empty iterator, even if `I::default()` would not be empty.

This PR changes `Fuse`'s `Default` implementation to match the documentation. This will be a behavior change for anyone currently using `Fuse::<I>::default()` where `I::default()` is not an empty iterator[^1], as `Fuse::<I>::default()` will now also not be an empty iterator.

(Alternately, the docs could be updated to reflect what the current implementation actually does, i.e. returns an always-exhausted iterator that never yields any items (even if `I::default()` would have yielded items). With this option, the `I: Default` bound could also be removed to reflect that no `I` is ever created.)

[Current behavior example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=a1e0adc4badca3dc11bfb70a99213249) (maybe an example like this should be added to the docs either way?)

This PR changes publicly observable behavior, so I think requires at least a T-libs-api FCP?

r? libs-api

cc rust-lang#140961

`impl<I: Default> Default for Fuse<I>` was added in 1.70.0 (rust-lang#99929), and it's docs and behavior do not appear to have changed since (`Fuse`'s `iter` field has been an `Option` since before the impl was added).

[^1]: IIUC it is a "de facto" guideline for the stdlib that an iterator type's `default()` should be empty (and for iterators where that would not make sense, they should not implement `Default`): cc rust-lang/libs-team#77 (comment) , so for stdlib iterators, I don't think this would change anything. However, if a user has a custom `Iterator` type `I`, *and* they are using `Fuse<I>`, *and* they call `Fuse::<I>::default()`, this may change the behavior of their code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-iterators Area: Iterators C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants