Update
After discussion here and elsewhere, there’s a new post with a more complete proposal and fresh comment thread. Please head that way!
I had a lengthy conversation with @josh last week about the Rust 2018 module design, and we uncovered a new variant that might be worth considering (and implementing) as we gain experience with the preview.
The design as it stands
To recap, in Rust 2018’s current design:
-
use
statements take fully qualified paths. - A fully qualified path starts with the name of an external crate, or a keyword:
crate
,self
, orsuper
. - Outside of
use
statements:- Fully qualified paths work, and have the same meaning as in
use
, unless a local declaration has shadowed an external crate name. - Paths may also start with the name of any in-scope item, i.e. an item declared or
use
d in the current module.
- Fully qualified paths work, and have the same meaning as in
I won’t give the full rationale here, as it’s not vital for understanding or weighing the new idea.
A possible variant
The main insight is that we can make relative paths in use
more ergonomic in a simple way:
-
use
statements take module-local paths. - A module-local path starts with the name of an immediate submodule (not any other declaration), the name of an external crate, or the keyword
crate
orsuper
(noself
)- If a submodule and external crate name conflict, the submodule shadows the external crate. We can provide an additional syntax (like leading
extern
) to instead get the external crate, if necessary, but the situation should be rare.
- If a submodule and external crate name conflict, the submodule shadows the external crate. We can provide an additional syntax (like leading
- Paths outside of
use
work the same as in the current design.
In this variant, paths in use
statements and paths outside them work almost the same way; the main difference is that the “module-local paths” in use
statements start with a crate or submodule name, whereas more general paths can also start with anything else that happens to be in scope. For example:
use std::collections;
// this doesn't work, since `collections` is neither a crate nor a submodule of this module
use collections::Vec;
// this works, though, because `collections` is in scope
fn foo() -> collections::Vec<u8> { .. }
Tradeoffs
Benefits of module-local paths
-
Ergonomics. It’s common to forget
self::
when writing a relative import, I think in part because it’s common to expect relative paths to work – especially since they do work outside ofuse
statements. -
Familiarity. This setup is much like paths at the CLI: relative by default, with a set of “prelude-style” names always in scope (including the actual prelude and extern crate names), much like
$PATH
. It’s also an approach taken by other languages, including Python2. -
Uniformity. While this doesn’t go the full distance toward making
use
paths the same as paths everywhere else, it addresses the most common remaining source of friction when trying to “hoist” paths intouse
statements.
Costs of module-local paths
-
Non-locality. In the current Rust 2018 design,
use
paths are always fully qualified, meaning that the path alone tells you 100% of the information needed to interpret it. There’s never any question where the path is rooted. Given thatuse
statements are a key way of understanding the bindings in a module, this clarity seems potentially valuable. (The counter-argument is that one is generally aware of submodules when working in a module, just as one is aware of other local declarations.) -
Shadowing. Because module-local paths are not fully qualified, there’s an opportunity for conflict (between a crate name and a submodule name). We’d almost certainly want to shadow the crate by the submodule (matching expectations from other similar path systems), but that then entails a means of disambiguation. OTOH, we can plausibly deprecate
self::
at that point.
Name clashes in general
One particular issue to draw out: in general, the way that use
paths and paths elsewhere differ can cause confusion when there is overlap in names. For example, in the current Rust 2018 design, we have:
// This is in the current Rust 2018 design
// this refers to `MyVec` from the *external crate* `collections`
use collections::MyVec;
mod collections {
struct MyVec { .. }
}
// this refers to `MyVec` from the *submodule* `collections`
fn foo() -> collections::MyVec { .. }
Probably we should lint against any situation in which a local declaration shadows an external crate name.
The variant design using module-local paths, OTOH, resolves the example above (because the use
statement would now refer to the submodule), but still suffers the problem with other kinds of declarations:
// This is in the variant design
use std::collections;
// there's no submodule `collections`, so this refers to a `collections` crate
use collections::MyVec;
// OTOH this refers to the `std::collections` submodule
fn foo() -> collections::Vec<u8> { .. }
It’s not clear to me how much we should be worried about these kinds of cases – we can lint against all of them. However, if we wanted, there are a few ways we could avoid these issues:
-
Make “fully qualified” be fully explicit. We could have all
use
statements start with an explicit designator of the root, e.g.extern::
,crate::
, etc, so that they would all have precisely the same meaning outside ofuse
statements. But of course that would be far more verbose, just to avoid a weird edge case. -
1path: we could fully unify the paths for
use
statements and otherwise. That has the downside that you can “cascade”use
statements, likeuse std::collections; use collections::Vec;
which is confusing in its own right. Not to mention the implementation challenges.
A conservative route?
It seems worth considering whether there’s a tweak to the current design that would be forward-compatible with this one. The main problem is with conflicts between submodule and crate names, which under the current design would lead to use
paths being interpreted as referring to external crates, and in the variant would be shadowed by the submodule name. To leave open forward-compatibility, we’d have to produce an error in such cases, and require use of some disambiguating syntax.