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? # for a free GitHub account to open an issue and contact its maintainers and the community.

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

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