Anchored and Uniform Paths

Rust 2018 is almost out the door, but there is one big decision the language team has yet to make. It has to do with the modules and paths system, so of course it is a very easy decision that no one has a strong opinion about. ;-)

In Rust 2018, we’ll be making some big changes to how paths work to try to create a more consistent experience. The “lodestar” (if you will) of these changes is an idea we call “1path:” the idea no matter where you are in your project, whether in a use statement or normal code, a path is interpreted the same way. In Rust 2015, the underlying path model is actually quite elegant, but it has produced confusing results in which paths seem to work differently in the root file of your project from other modules, and in which the difference between use statements and other paths are often surprising.

With Rust 2018, we’ve gotten a lot closer to “1path,” and we’ve settled on two possible variants to stabilize on. The current behavior of 2018 is forward compatible with both variants, but since that means it has the downsides of both and the upsides of neither, we’d like to decide on one variant before we ship the 2018 edition in December.

The two variants are called anchored paths and uniform paths. They share these features in common that are different from how thins work in the 2015 edition of Rust:

  • Modules within your crate are accessed under the crate:: namespace, instead of being at the top level (e.g. use crate::module::Foo;)
  • Dependencies on other crates can be used without adding extern crate statements.
  • You can access other crates as well as the crate:: namespace from anywhere in code without a leading :: - that is, no more impl ::std::fmt::Debug, you can just write impl std::fmt::Debug.

However, they differ in these respects:

  • In anchored paths, if you want to import something from a submodule, you nede to use self::submodule:: (like the 2015 edition). In uniform paths, you can just use submodule::.
  • In uniform paths, of a submodule and an external dependencies have the same name, to depend on the external dependency, you need to disambiguate it from the submodule using use ::foo.

Uniform paths have the immediate advantage that they get us closer to 1path than anchored paths: paths work the same way within the module as in use statements, getting rid of the often confounding use self:: syntax. However, anchored paths have some advantages over uniform paths as well:

  1. The use statements of anchored paths are syntactically disambiguated as to where they come from: either they come from a special namespace crate, super, and self, or it comes from an extern crate. This makes it easy to make sure your use statements are grouped together properly during code review without referencing other information.
  2. The rule for disambiguating conflicting names is the only time in the new system when you have to use leading ::. This makes it a bit surprising when you do run into it, and makes conflicting names feel discouraged and unidiomatic (there’s debate within the lang team about whether they should be idiomatic or not).

Over time, most of the language team has come around to being inclined toward uniform paths, because getting closer that 1path property seems very beneficial. No longer will paths work differently between use statements and other purposes, within a module, all paths resolve exactly the same way! That’s very cool.

In our meeting yesterday, I was part of the minority that favored anchored paths. The dissensus came down to disagreement over whether the drawbacks I listed above overcame the uniformity benefit. There was general agreement that the benefits of both anchored and uniform path were real, but we’ve agreed there’s no way we can think of to get all of the benefits without a new drawback we’ve agreed is less preferable (like always disambiguating dependencies and internal paths with another new syntax).

In the evening and day since the meeting, I’ve come around to thinking that we ought to move toward stabilizing uniform paths. Considering the advantages and disadvantages, I think:

  1. The uniformity is a really big advantage, and the specific feature we’re changing - no longer having to use self:: - is something I know is a big stumbling block for new users and a big annoyance for advanced users (I’m always having to edit and recompile because I tried to import from a submodule without using self).
  2. I do encounter conflicting names regularly, but still somewhat rarely, and I think we can accept the use :: papercut in exchange for getting rid of the use self papercut, which is encountered more often.
  3. I think the objection about organizing use statements holds up, but on consideration, its fairly unlikely for someone to get this wrong (I believe contributors will quickly learn the conventions), and the consequences of an import being sorted incorrectly is occassional minor annoyance, not showstopping: you look for the import’s definition in the wrong place, realize that its missorted, and hopefully fix it.

I don’t want to move too quickly, but there is a time pressure here - ideally we’ll have reached a final decision on this question within the next 3 weeks. I’m making a proposal that we stabilize uniform paths on 2018 (with a beta backport to 1.31), and anyone with strong opinions about this should leave their feedback. However, keep in mind that at this phase, any alternative other than anchored and uniform paths is out of scope for the 2018 edition, so other proposals just can’t be considered (unless they are a backwards compatible variation on one of those proposals, then maybe). You can find the GitHub discussion thread for this proposal here.

Overall I’m really excited about Rust 2018 and I believe either version of the path proposal will be an awesome step up over what we’ve had in 2015. We’ve come a very long way from my first blog post about modules nearly 2 years ago, thanks so much to everyone who has put in so much work on this problem!