Cautiously consolidating the standard library

One thing that has come up a number of times in discussions about a new prelude is the current layout of the standard library is not always great. One suggestion is to add more types to the prelude so they're always there. However this has proved controversial.

So what if we use re-exports in another way? For example. As bluss and others have mentioned, re-exporting more functions or types to the std root. Or perhaps simply consolidating more under the same modules. E.g. Path and File are intimately connected yet they're exported from separate modules. They could be conveniently consolidated in the std::io module, which they'd share with related types and traits.

5 Likes

I think this makes sense, and I intend to cover this possibility in the 2021 prelude RFC (or the outgrowth of that for non-edition-requiring changes). I did mention it in my feedback summary, I think, and it makes sense to at least mention this in the alternatives section and get the libs team to consider it holistically with the other options on the table.

2 Likes

Maybe? Currently use std::fs::Path generates an awful error:

   Compiling playground v0.0.1 (/playground)
error[E0603]: struct `Path` is private
  --> src/lib.rs:1:14
   |
1  | use std::fs::Path;
   |              ^^^^ private struct
   |
note: the struct `Path` is defined here

but I'm not sure how confusing it would be if Path existed in fs and maybe even io. Wouldn't people wonder if that's the same Path or are they different types? And not know where to look for actual Path docs.

There's also Pre-RFC: Expressive Standard Library Paths, which was a variant of this in a way. I'll repeat what I said there, because I think it applies here:

I've always thought that most of the submodules that std exports felt like irrelevant implementation details that were being leaked to the public API. There are submodules that only have one type/trait they export (e.g., std::default ), types/traits in submodules for reasons that aren't obvious to me (I'm still not sure why ManuallyDrop and MaybeUninit are in the mem submodule), and types/traits that are split across multiple submodules rather than being grouped together (e.g., some operators live in core::cmp and others live in core::ops ).

I'd love to be able to write std::HashMap instead of having to remember it's in collections, among many other things. And I agree with kornel that having multiple paths to the same type/trait can lead to confusion because it's not immediately clear if they're the same or different.

When conversations like this come up, I have two questions:

  1. If we were to go back in time and start over, what would std's public API look like?
  2. Given that we can't go back in time and the cat's already out of the bag, what would a realistic compromise or solution look like?
12 Likes

What are the constraints here? I can see not existing crates consuming the std, but I'm wondering if there are any oddities for people targeting custom platforms or in no-std land where the use of std is atypical compared to someone writing for std on a widely supported platform.

I wonder if it'd be possible to not directly expose std to consumers, but instead expose a facade that reexports std with a better structure. Except we could have multiple facades, one per edition (well, maybe 2015 and 2018 would use the same). Of course, we might not want to do that because of the confusion it could cause for beginners if they come across imports for an older edition in documentation when they're on a newer edition.

I'm sure there must be an idea like mine floating around somewhere - I doubt my idea's very original :slight_smile:

Isn't that effectively the same as the prelude? You could just define modules in the prelude and have those re-export modules/items in std. So long as the prelude imports override std imports, it wouldn't matter where the modules in the prelude are actually located.

EDIT: nevermind this. I forget that prelude imports don't override std::...

I don't think the documentation is too bad for reexports, is it? Though I do think their use should be minimized. If it were decided (for example) that std::io::Path was now the canonical location, then the docs for std::path would contain:

pub use std::io::Path;

I do agree that it could be confusing in code during a transition period or for someone reading older code. This can be mitigated somewhat by IDEs and related tools but I accept that's not a totally satisfying answer.

To this day, I am still confused if futures::future::Future and std::futures::Future are the same type or not (and if they are the same type, are they interchangable). If Path existed in two different modules, the docs I think would need to very clearly state that these two paths are actually the same, and that they developer can use them interchangeably. Maybe rustdoc itself could do something to make this clear as well.

You can use doc(no_inline) to make it very clear it's a re-export:

#[doc(no_inline)]
pub use std::path::Path;

image

5 Likes

Re-exporting everything to the top-level is interesting. There's currently a discussion about adding more types to the prelude, but maybe if std::HashMap and std::Arc existed, there wouldn't be a need to add them to the prelude.

2 Likes