Summary of ideas at the bottom.
Leading ::
I’ll confess that I come from a C++ background, so my biases are that way. In C++, namespaces follow lexical scoping, with leading ::
to make an absolute path starting at the global namespace, so this means that, say std
always means ::std
unless it is shadowed. This means that you can just write std::string
and it will mean exactly what you think it means. To me, ::
is a disambiguator that you apply only when you really need to, and it’s not that I think it is inherently ugly (it’s not), but rather that it goes against my sensibilities of how paths “ought” to work. Why force people to disambiguate with leading ::
when there is not possibly a second interpretation? The C++ model wouldn’t work in Rust, because of the way modules are scoped, but this is where I come from.
I do think that requiring crate::
everywhere would be ugly, but to me it feels a lot like std::
except that crate
is a magic word like self
. In fact, I think that the current crate name should be declared as an alias for it, rather than requiring the use of crate
, to resolve the asymmetry.
Searching names
I also don’t think that there’s any difficulty understanding where a name comes from with the originally-proposed solution. I do understand the value of this, as I also code in Go, where source files can share scopes, and it’s extremely useful there. But in Rust, because of the lack of shared scopes, we don’t have to look far beyond the file. I do not expect that users will often run into trouble because of not knowing which packages are imported, or the contents of the prelude.
The only things that can muck this up are macros and glob imports, but your proposal doesn’t address them, and if we address them, then we have the following easy algorithm:
-
Ctrl+F
the file. If you see an in-scope definition of the name, then you have found it. - Check the list of crates and the prelude. If the name is found anywhere there, then you have found it (in case of ambiguity, the crate name takes precedence over the prelude).
- If I still haven’t found it yet, then it must be imported by a glob or declared by a macro.
Step 2 is not hard as the list of possible identifiers is not that big, most developers will have internalized the contents of the prelude, and I don’t expect developers to regularly lose track of which projects a crate uses (it will very slightly increase the overhead of reading unfamiliar code, but as the developer seeing an unfamiliar crate name would probably go check out Cargo.toml
to look at the package anyway, I don’t think it’s a big cost).
Ambiguity
After thinking about my ambiguity-resolution proposal more, I realized there is an issue with making an ambiguous import an error: it makes any change that introduces a new ambiguity a breaking one. This could arise the following ways:
-
Declaring a new name locally. This is fine, since it’s a change in the code in question.
-
A macro declares a new name locally. This is not fine, since it could be a change upstream breaking a downstream client. This is already a breaking change, however, because this new name can already shadow something imported by glob or in a higher scope, or conflict with another locally-declared name.
The fact that this is a breaking change for a macro author may need to be clarified, however.
-
Another crate adds a new name, which is glob-imported. This is not fine, since it can cause this name to shadow another name, or to conflict with a name in another glob import. The only new case here is that a glob import could shadow a crate name (it’s already possible for it to interfere or shadow a local/prelude name).
This does cause me concern. As it is, glob imports can be used without worry about backwards-compatibility if a) crates are careful not to introduce anything with a name used in the prelude b) a module is careful to use a glob import only once, and only at module scope. This would mean that either glob-imports impose more burden on crate authors (they cannot introduce new names and be backwards-compatible) or on glob-importers (they cannot assume any sort of backwards compatibility if they use relative paths any more).
However, there’s an easy fix: we can simply ban names imported by cross-crate glob as leading path segments, and require users to import them explicitly. This could be applied only in
use
, but I think the ergonomics are better if it’s applied uniformly. This rule only needs to apply when the import and declaration are in different crates; if they are in the same crate, then there are no issues because it’s all under one author’s control. -
Adding a new crate. This is fine, since it’s a change in the code in question.
-
Adding a name to the prelude. This is a Big Problem if this introduces an ambiguity error: the change is not backwards-compatible. I believe that, today, adding new names to the prelude is non-breaking since they can always be safely shadowed, and we should probably preserve that.
Bearing the above in mind, there are at least two kinds of circularity problems that we obviously wish to fix. I thought that things would not be so bad initially, but now that I have considered them in more detail, the implications are very thorny.
Macros
Macros can ambiguity in the invocations of other macros and other use
declarations. @nikomatsakis gave this example, which I don’t see addressed in any proposal allowing crates to be referred to with non-absolute paths. In this example, foo
is a crate.
use foo::bar;
baz!(); // declares "mod foo"
How do we resolve this import properly? We would have to wait until the expansion of baz
to understand it. I propose two variants two point out worse nastiness:
use foo::bar;
bar!(); // declares "mod foo"
This one is worse, because we can’t lookup bar
if we hold off on resolving the import until after the macro is expanded. In this next one, foo
is not a crate:
use quux::foo;
fn local() {
use foo::bar;
baz!(); // declares "mod foo"
}
This one demonstrates that “locally-declared” as a separate category in handling ambiguity is not enough to actually manage things: any case where we can shadow is enough (glob imports is another one) to cause it. And of course we can also mess with macro names themselves:
use foo::{bar, baz};
fn local() {
bar!(); // expands to "use quux::baz;"
baz!(); // expands to "use quux::bar;"
}
This ambiguity is extremely broad in scope, and hard to solve. Moreover, it gets even worse if we ever make it possible for macros to perform name lookup, which is required if we want to make the intert attributes used by proc macro derives to scope properly. @aturon implied in the first post that the proposal there would resolve issues like this, but it’s not clear to me how broad of problem he was trying to fix there was, so I’m not sure if these were in scope.
Circular use
mod foo {
use bar::*;
}
mod bar {
mod bar {}
}
As before, I don’t actually think that @aturon’s proposal is enough to resolve this, because the local shadowing behaviour of glob imports causes issues that go deeper than simply shadowing crates. Globs have something in common with macros, in that they can declare things which aren’t immediately apparent from surface syntax. But in fact, we don’t need that to create ambiguity:
mod bar {
pub(crate) mod baz {}
}
mod baz {
pub(crate) mod bar {}
}
mod foo {
use bar::baz;
use baz::bar;
}
Now which bar
and baz
does foo
get?
I think we would have to enforce the following rule, or something stricter that the compiler can reasonably calculate: the use
declarations and macro invocations in a crate form a DAG, when ordered by whether one can change the interpretation of the other. I’d have to think more about whether or not we can do this in a way that lets us still add things to the prelude (or crate authors add things to crates without breaking glob-importers), but honestly, I feel like we’re getting too far into the weeds for a change this late in the edition cycle.
(Personally, I’m starting to feel like the edition should be postponed.)
Side Thought: Tooling
From a tooling point of view, I am not sure that letting an IDE have instant ideas when you write a name is going to be a super useful idea. There is one thing that I would love to have, however, and that is goimports
. For those unfamiliar, because imported names are always qualified in Go, there is a tool which looks through your source for undeclared package names and adds them as imports. It’s not perfect but it definitely beats having to go back to the top of your source file manually all the time. Something similar for Rust would be wonderful: if I write some_crate::name
and some_crate
is undeclared, then I can run a tool to automatically check if some_crate
exists in Cargo’s index and, if so, add a dependency at its current version. This would only work if we can reliably determine that some_crate
is indeed intended to be a crate. We could do it heuristically, or only apply it to absolute paths with ::some_crate
(I, for one, am the sort of lazy programmer who would absolutely write the absolute path, run the tool, and then delete the leading ::
). I think, having written this down, that it is mostly orthogonal to the other questions in this thread, though, so I think future discussion of this should branch off rather than continue here.