Pre-RFC: Expressive Standard Library Paths

Summary

Introduce many re-exports to std to shorten overly verbose and repetitive path names.

Motivation

Paths in the standard library are currently structural over expressive; everything is organized into modules, and items generally only have one path. The only exception to this is std::collections, which re-exports its collections at the module root, but nothing else in the standard library does this.

This system results in very verbose and repetitive paths, such as std::vec::Vec or std::default::Default. This is especially noticable for items with common names, such as Error (which often has to be renamed to StdError to avoid repeating the long path) and in macros, where the user has to use the full paths. Code like ::std::default::Default::default() just obfuscates the actual intent of the code, and ::std::Default::default() or even ::std::default() (as implemented by default_free_fn) would be much clearer.

This change will also greatly improve the readability of code when absolute paths are used; while std::time::Instant cannot be read in plain English ("the standard library's Instant in its time module" is out of order and confusing), std::Instant can ("the standard library's Instant").

Furthermore it will reduce the mental overhead of having to know the locations of items; instead of having to remember whether Unpin lives in std::pin, std::marker or std::ops users will simply be able to write std::Unpin. This applies to many other types in the standard library too.

Guide-level explanation

Firstly, everything with obvious repetition in the path name, such as std::any::Any, std::future::Future or std::rc::Rc, will also be accessible from the crate root - std::Any, std::Future or std::Rc. Secondly, everything in the prelude will be accessible there, including std::marker::Send, std::fmt::Debug and std::convert::AsRef as std::Send, std::Debug and std::AsRef. Finally anything that has implicit repetition in the path name (for example std::collections::HashMap, as there's no such thing as a HashMap that's not a collection) will be there too - std::HashMap.

Reference-level explanation

The following items should be exported at the crate root of their appropriate crate:

  • core::any::Any
  • core::borrow::{Borrow, BorrowMut}
  • core::cell::{Cell, RefCell, UnsafeCell}
  • core::clone::Clone
  • core::cmp::{Eq, Ord, PartialEq, PartialOrd}
  • core::convert::{AsMut, AsRef, From, Into, TryFrom, TryInto}
  • core::default::{Default, default}
  • core::fmt::Debug
    • Not Display as it is not in the prelude.
  • core::future::{Future, IntoFuture}
  • core::hash::{BuildHasherDefault, BuildHasher, Hash, Hasher}
  • core::hint::unreachable_unchecked
  • core::iter::{DoubleEndedIterator, ExactSizeIterator, Extend, FromIterator, FusedIterator, IntoIterator, Iterator}
  • core::marker::{PhantomData, PhantomPinned, Copy, Send, Sized, Sync, Unpin}
  • core::mem::{ManuallyDrop, drop, MaybeUninit}
  • core::num::NonZero*
  • core::ops::{Drop, FnMut, FnOnce}
  • core::option::{Option, Some, None}
  • core::panic::PanicInfo
  • core::pin::Pin
  • core::result::{Result, Ok, Err}
  • core::str::FromStr
  • core::sync::atomic::{self, Atomic*}
  • core::time::Duration
  • core::lazy::{Lazy, OnceCell}
  • alloc::borrow::ToOwned
  • alloc::boxed::Box
  • alloc::collections::{binary_heap, btree_map, btree_set, linked_list, vec_deque}
  • alloc::collections::{BinaryHeap, BTreeMap, BTreeSet, LinkedList, VecDeque}
  • alloc::rc::Rc
  • alloc::string::{String, ToString}
  • alloc::sync::Arc
  • alloc::vec::Vec
  • std::collections::{hash_map, hash_set, HashMap, HashSet}
  • std::error::Error
  • std::fs::{File, FileType, create_dir, create_dir_all, read_dir, remove_dir, remove_dir_all, remove_file}
  • std::path::{Path, PathBuf}
  • std::sync::{Condvar, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc}
  • std::time::{Instant, SystemTime, SystemTimeError, UNIX_EPOCH}
  • std::backtrace::{Backtrace, BacktraceStatus}
  • std::lazy::{SyncLazy, SyncOnceCell}

In order to reduce confusion none of the above re-exports will be inlined.

Drawbacks

  • Perceived churn: This change could cause additional churn if users believe the old paths to be deprecated.
  • Long crate root documentation: The index.html of the standard crates will become a lot longer.
  • Confusion about which path to use: Now that types are accessible from multiple places, confusion could arise about which path to use.

Rationale and alternatives

  • Hide the documentation of the re-exports: This allows the index.html of standard crates to be shorter, but significantly reduces the accessibility of these exports, and will make it a quirk rather than a feature of the language.
  • Inline the documentation of the re-exports: This perhaps makes the items more accessible, but having multiple pages for the same item can cause confusion about whether they are the same thing or just look the same.
  • Do nothing: There will be less confusion as to which path should be used for an item, but user code will have many more imports, and macros will have to write out long repetitive paths.
  • Re-export a different set of items: This is always possible, the list above is not final.

Prior Art

  • C++ has most of its types defined at the root of std, for example std::vector.

This section is incomplete, please respond with more ideas.

Unresolved questions

  • What changes should be made to the list above?

Future possibilities

None I can think of.

8 Likes

But when does one actually need to write such code outside of a proc-macro (where it's invisible to human consumers of the code)? Many of your examples fail the motivation because they are already in the prelude. You don't need to write std::vec::Vec, you can just write Vec, today.

7 Likes

That specific code was for the macro example, but there are many cases when non-macro code can be made more concise and clear, like my Error example. impl std::Error for MyType is so much better than impl std::error::Error for MyType or use std::error::Error as StdError; impl StdError for MyType.

Another example: I might have multiple HashMaps in scope, and being able to refer to the std one as std::HashMap is much better than typing out the full std::collections::HashMap or importing a StdHashMap.

2 Likes

Why don't you just import the relevant types then? Outside of proc-macros there isn't really a reason not to do that. In fact an argument can easily be made that that is what use statements are for.

3 Likes

Part of the goal of this was to reduce the number of use statements. Rust requires too many imports and it's one of my least favourite parts of the language. Plus use statements don't fix the issue: StdError or error::Error are uglier and less clear than std::Error.

5 Likes

Ok, that's your opinion and you have a right to it.

But imagine what would happen if the rust language was changed every time someone came round and complained that they didn't like some aspect of the language. Note that that is different than an objective shortcoming of the language.

1 Like

All changes to Rust are opinionated, you can't have an objective change. I posted this here to get technical feedback on my opinion not to be told that it shouldn't be implemented simply because it's an opinion.

7 Likes

Please don't put words in my mouth. I said an objective shortcoming. I said nothing about an eventual change that addresses that shortcoming.

Maybe I misunderstood, but I thought you criticised my proposal for change for being opinionated, so the natural conclusion is that other proposals and changes (which I assume you support) would be not opinionated. And if you weren't criticising my proposal for being too opinionated I don't know what that reply was about.

It was about making you aware of the difference between proposing a feature because you like it v.s. proposing a feature that addresses a real problem.

1 Like

Ah, sorry for the misunderstanding.

Still, I do not believe there to be a difference between the two. This change was proposed because of a problem I perceive to exist, just as all changes are proposed because of a problem perceived to exist by the author of said proposal. That this is a problem may be an unpopular view and could be very wrong - that's exactly why I posted it here, so that I could find out.

The rapid fire back-and-forth isn't necessary. If any of find yourself in that situation, then please ask to take it offline.

@Kestrer I am also personally not a huge fan of using the word "objective" in discussions like this, for a variety of reasons. However, I do agree with a tweaked version of what @jjpe is saying here. That is, as a member of the library team, in order for a change like this to be accepted, I'd want to see some kind of evidence that the problem you are experiencing is:

  1. Felt by many others to a similar degree.
  2. Solved by the solution you put forward here.

Because for me personally, the problem you're experiencing is not one that I've experienced.

14 Likes

Personally, I would be happy to see some (but not all) of these types added to the prelude in the 2021 edition, so they don't need to be imported at all. I think in the first half of next year, if edition-specific preludes are implemented, we may have a discussion about what types we would add to the 2021 prelude.

26 Likes

Well, @Kestrer can have my +1. When writing new code, I hate that whenever I mention a new std type for the first time, I have to break my flow, jump up to the top of the file, and type out a wordy use statement. (I have the same issue in C/C++ with #includes.)

I could probably work around the problem with rust-analyzer’s auto-import feature, but I don’t currently use rust-analyzer, and in general I think languages shouldn’t depend on the presence of an IDE for their ergonomics unless absolutely necessary.

5 Likes

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).

In other words, have another +1, @Kestrer.

9 Likes

:+1: -- given that types in the prelude can be overridden (type Vec = u64; may be bad style but it works) it'd be nice to get more of the particularly-well-known things usable without use imports.

(Traits too, but those are more complicated -- as you of course know and alluded to in the unquoted part of the post.)

Changing the prelude doesn’t help alloc users (unless we get some way for dependencies to inject into the prelude too) or the Error case.

I feel the same issues that prompted this discussion, there is too much unnecessary redundancy in use alloc::vec::Vec; and

use std::error;
impl error::Error for Error {
  fn source(&self) -> &(dyn error::Error + '_) {
    ...
  }
}
3 Likes

I'd love to see that as well.

For types (as opposed to traits), I think we wouldn't need edition-specific preludes. Adding a trait to the prelude can break existing code, so it'd need an edition boundary. To the best of my knowledge, adding a type or function to the prelude cannot break existing code, because if you define your own it'll override the one from the prelude.

I also agree with the original post, and would be happy to see most (not all) of these types added to the top-level. I don't think any of the functions should be, though.

New traits in the prelude are an allowed breaking change. They don't require an edition boundary. For example, see Unpin.

They're allowed, but in practice, we've declined to do so in the past when it would cause widespread breakage. For instance, adding TryFrom and TryInto to the prelude would break anything that used non-std versions of the same trait methods.