There has been some previous discussion of ways to make the module system easier to use. While there wasn’t much consensus around any of the proposals in those discussions, one position that did have rather a lot of support was this: a common source of confusion is that use
takes paths from the crate root instead of the local crate.
Previous proposals I had made about mod
did not address this point of confusion at all. Mainly this is because I found no change that was all of: a) backwards compatible, b) simple to understand, c) an improvement over the status quo. However, if we weaken our restriction around backwards compatibility (and I’ll get to that in a second), I think there is a proposal that is both simple and a clear improvement over the current proposal.
With “import statements” (the use
keyword) we perform name resolution in two steps:
- First, we look in the local module. (
self::
) If this name is not found, move to step 2. - Second, we look in the crate root (
::
).
This would mean that any path which is valid in a module is also valid in an import statement in that module, but that import statements would also be able to bring in items from the crate root (without any ‘absolute path’ boilerplate). This seems to match users’ naive intuitions about what a use statement should do.
(Of course it would mean that use
paths would be to some extent more contextual than they are today, since local modules could shadow crate-root modules. This is just lexical scoping though and affects most parts of name resolution today, just not use
statements.)
But this would clearly be a breaking change. So how could we go about implementing it?
Pivoting on a new keyword
The most conservative way to make the change is to introduce a new keyword which has the semantics we want, and make no change to use
. For example, we could introduce the import
keyword:
mod foo {
mod bar {
struct Bar;
}
// works like use
import baz::Baz;
// also imports from submodules (and re-exports)
pub import bar::Bar;
// still an error:
// use bar::Bar;
}
mod baz {
struct Baz;
}
The big downside of this approach is that we now have two keywords with very similar but subtly different semantics. Its no fun to have two tools in the language that do almost the same thing, and to tell people they just shouldn’t use one.
The way to solve this is the “epochs” proposal - we would add the import
keyword sometime in the current epoch, and then remove use
during the epoch shift. Possibly some day, if we feel we like the use
keyword better than whatever alternative we use, we might reintroduce use
as an alias. Maybe at another epoch later we would remove import
, completing the pivot.
Relying on a mechanical change over
A slightly more radical way to perform the change is to perform it suddenly as a breakage at the epoch point. All current use statements can be mechanically transformed to work under the new semantics: the naive approach would be to prefix all non-self
uses with ::
, a slightly more advanced approach would be to check the AST for submodules that could be shadowed and only insert the ::
if they would be necessary.
This has the downside of being non-incremental. There would be no ‘shared subset’ that worked the same on both epochs, so you couldn’t make the change to support the new epoch in advance of the epoch shift. However, it avoids having redundant forms in the language.
Conclusion
Ultimately, all three options - keep the status quo, new keyword, mechanical transformation - have downsides & its not obvious which would be the best one. I wanted to write this up both to put the idea of making this shift out to the world, and to use it as a case study for the kinds of changes and the strategies to implement them we could consider during an epoch shift.