The Great Module Adventure Continues


Personally, I find the keyword-based approach (approach 1) preferable because:

  1. Very readable
  2. Very Google-able
  3. Very grep-able
  4. It’s seems really elegant that a leading extern block naturally documents external dependencies in the crate. In the current state of Rust, I try to defer even showing dependency usage when thinking about how to present Rust because there’s a lot of room for confusing students right at the beginning. Taking approach 1 would make Rust modules something I wouldn’t feel the need to hide because of noise.

I don’t particularly care which keyword is used if approach 1 is adopted. I have no objections to extern, but I realize it’s “long”.


Thanks @nikomatsakis for that writeup.

After the meeting, I reflected a bit more on my preferences between the two broad options you laid out, and feel that:

  • The downsides to leading-extern are relatively mild.
    • While I’d be sad to lose the “absolute paths always start with a crate designator” property, I think having extern and crate prefixes is very clear.
    • I think that use extern::{ /* all your imports */ } is a wonderful codification of the “external imports” vs “internal imports” split that most Rustaceans do already.
    • I don’t think the inline use is so bad: extern:: vs :: for an occasional use. @withoutboats points out that using this multiple times in the same line isn’t great, but I feel like that’s a bit of an antipattern anyway, and we should work to address the API issues that make it necessary.
  • The sigil option is very risky, because Rust is already fairly sigil-heavy, and we’re talking about putting this new sigil in a large chunk of use statements – the very first thing you tend to see when looking at Rust code. It really feels like it could be a “shark-jumping” moment for Rust. I think we should only consider it if we can find a “slam dunk” sigil that feels highly natural.


To follow up on that last bit:

I think @std and [std] are much too noisy to be seriously considered. Consider:

use @std::prelude::v1::*;
use @std::cell::{RefCell};
use @std::rc::{Rc, Weak};

use @futures_core::{Future, Poll, Async, Stream};
use @futures_core::task::{Context, Waker, LocalMap};
use @futures_core::executor::{Executor, SpawnError};
use @futures_util::stream::FuturesUnordered;

use crate::thread::ThreadNotify;
use crate::enter;

I personally think that trailing : as a separator is the most promising sigil choice – it looks natural, it’s concise, and with just a bit of syntax coloring (showing the crate name in a different color) has minimal ambiguity risk:

use std:prelude::v1::*;
use std:cell::{RefCell};
use std:rc::{Rc, Weak};

use futures_core:{Future, Poll, Async, Stream};
use futures_core:task::{Context, Waker, LocalMap};
use futures_core:executor::{Executor, SpawnError};
use futures_util:stream::FuturesUnordered;

use crate:thread::ThreadNotify;
use crate:enter;


Some comparisons:

One thing that’s hard to account for is syntax highlighting. Here is a shot of my emacs buffer for suffix-:; the emacs mode happens to syntax highlight foo::



My take away from this exercise was that leading-extern and suffix-: were the only viable options, really.


One thing that bugs me visually about the : separator is when your importing the whole crate level:

use std;
use regex;
use crate:mymodule


use extern::std;
use extern::regex;
use crate::mymodule;

Although for my use cases it would probably be like

use extern::std::fs;
use extern::std::io;
use extern::regex;
use crate::mymodule;

instead of

use std:fs;
use std:io;
use regex;
use crate:mymodule;

I like that the extern marker is there in each use-case.


Even this little bit of code helps, nice.

I’m finding myself being in favor of leading-extern even more afterwards. Or rather, it’s the one that seems fine, while the others seem not very good.


What happens if a crate is extern crate'ed into a module (or function) rather than the crate root? Then the external crate is not at the root as all the suggestions seem to assume.


Would you even be allowed to write this? My assumption for the suffix-: syntax was that having a module after the crate name would be required, so probably to import a crate’s root module you’d write something like

use regex:self;

static foo: regex::Regex = ...;

Although, would you ever actually need to import an external crate’s root module? That just causes you to need to write an additional : after the crate name later on compared to just using an absolute path:

static foo: regex:Regex = ...;

This does make me consider what using crates that are intended to be brought in just with their root module would be like, I have one such crate so here’s what its example looks like currently:

extern crate bs58;

use std::io::{ self, Read };

fn main() {
    let mut input = Vec::<u8>::new();
    io::stdin().read_to_end(&mut input).unwrap();
    println!("{}", bs58::encode(input).into_string());

with suffix-::

use std:io::{ self, Read };

fn main() {
    let mut input = Vec::<u8>::new();
    io::stdin().read_to_end(&mut input).unwrap();
    println!("{}", bs58:encode(input).into_string());

and with leading-extern:

use extern::bs58;
use extern::std::io::{ self, Read };

fn main() {
    let mut input = Vec::<u8>::new();
    io::stdin().read_to_end(&mut input).unwrap();
    println!("{}", bs58::encode(input).into_string());

I dislike the fact that suffix-: allows very subtly using an external crate without having a use statement for it (under my assumption of how it operates).


I assume that’s because this is a continuation of RFC 2126 which has:

Presumably since it’s just a lint these new syntaxes would have to handle that situation somehow, which I think would just end up treating the external crate’s root module the same as a local module at the point it’s imported.


Most of my top level imports are actually root module imports. For item imports I mostly use them for trait imports inside functions. So for me it would look like this:

extern crate bs58;

use std::io;

fn main() {
    use std::io::{ Read };

    let mut input = Vec::<u8>::new();
    io::stdin().read_to_end(&mut input).unwrap();
    println!("{}", bs58::encode(input).into_string());

The self version might work, although it would be odd that use regex:self and use crate:foo have their keywords on opposite sides, and only one requires the self import, and consistent usage use crate:foo::self is redundant.


That’s… deeply disappointing. The main reason I brought up old Stack Overflow code was a concern about churn- I am not convinced that deprecating all old paths is any better than changing the meaning of use foo from a top-level item to a dependency. Leading-crate actually preserves the meaning of use dependency::foo, which I suspect is even more overwhelmingly common in examples than in actual crates.

I would even prefer simply keeping the status quo to any of the leading-extern/sigil proposals. They all feel worse than where we started, like putting edge cases on a pedestal above common cases. Is handling those edge cases (with nothing more than a deprecation) truly worth losing out on minimal churn, minimal noise, “absolute paths start with a crate”, similarity with other languages, and (yet again) the consensus from the RFC?

I know I’ve objected to this framing before, but I really don’t think 1path is that important. It was always a peripheral concern, when it was even present, in previous blogs and threads and RFCs. I don’t think it would be at all a shame not to get it, as long as we still solve a) path confusion and b) extern crate/use redundancy and c) pub-as-pub(super)-vs-pub(crate) reasoning and d) “some files are non-leaf nodes” confusion (via renaming and e) “where do use paths start from” confusion and f) all that without drastically changing how paths look.

…whew, now that I have that out of my system: If we can’t manage leading-crate or at least leading-::, and we can’t manage to fix the original problems some other way/keep the status quo, then I prefer trailing-: or something similar, despite its ambiguity with type ascription and confusion with ::. It has the least “wall of sigils” effect, and it doesn’t make paths any longer than really necessary to solve the original problems, at least if we can do it without use dep:self.

To summarize my opinion: I was excited about the RFC/leading-crate; I’m fine with the status quo though I recognize it causes confusion; but leading-extern or sigils are worse to use and feel like design-by-committee (apologies :confused: ) sacrificing usability on the altar of edge cases.


You know just how to make a team shudder!


Thanks, @rpjohnst, for the full-throated defense of leading-crate. I will definitely mull on it.

One thing that would be helpful to clarify: reading your post a couple of times, it seems like the primary downside you’re citing to the other options is that they are a more radical departure for paths. After all, literally every use statement would have to change, rather than only those drawing from the current crate.

What I’d be curious to know, though, is how you feel if you imagine the idea in a vacuum, not thinking about the change involved.

To me, the ability to have a single, unified syntax of paths with a consistent meaning in all locations seems like a real win – not to mention never typing use self::whatever::thing again. While I agree that we can address many points of confusion without taking this 1path step, I don’t see in your post a rationale for why 1path seems so unimportant to you. Is it just by comparison to the shift in path syntax? Or something else?


I think it’s not only the amount of change involved, but also the total complexity it requires (at least in my own mental model).

The current system has a single syntactic form: a sequence of segments separated by ::s. The empty segment is the root, and use paths start there because they are primarily used to reference things “above” the current module. (Though you could write use ::std::io if you wanted.)

Leading-crate feels like a simplification here. The current system has two ways to get to a crate’s top-level items: go through the root, or go through some extern crate item. Leading-crate instead puts all crates in one place, so you always go to the root. Here, uses are still nice, and paths still have a single syntactic form.

Leading-extern feels like a complication, or a move sideways at best. It includes the extern:: mechanism just for dependencies- at least extern crate just reused the “item” concept. Here, there are still two ways to get to top-level items, and there’s an extra concept, though paths still have a single form.

Sigils feel like a complication. Instead of all paths being simple sequences of segments, some segments are different. This new mechanism is even weirder than extern:: or extern crate, and is undoubtedly unfamiliar. Here, there are two types of crate roots, an extra concept, and paths have multiple forms.

I feel like this also explains my feeling about 1path- I like that use's default is the root, because it’s used that way so much more often than for not. I like making the concept of “go to the root” a part of use more than I like it as part of an extra concept attached to paths.

(As always, thank you so much for bearing with me and my stubbornness! I really appreciate it.)


On another note, I believe leading-extern doesn’t quite achieve 1path! It has the same ambiguity around the crate visibility modifier as leading-crate, and so would require ::crate::foo::bar in tuple struct types while permitting use crate::foo::bar in imports.


To be fully honest, I don’t exactly see how leading-crate/leading-::'s fallback mechanism is challenging (though I do see the merit of avoiding fallback). It could be handled much the same as glob imports are, and basically be “implemented” by allowing the crate keyword as the start of an absolute path as a no-op and taking RFC#2126–that is, “glob” importing dependencies into the root unless shadowed. (Ideally not allowing them to be used except by absolute paths.)

It’s maybe not the prettiest solution in the world, but it works as desired, and we have leading-crate. The only change required to enable leading-:: is to allow the crate keyword to come after a leading ::. The only further change required to get 1path is to require a leading :: where it’s “implied” by the absolute-path context (thus use leading-:: and not leading-crate).

If I’m wrong about something, please tell me. I think I got the model right, but it’s possible I missed something, or oversimplified.

That is an interesting minimal idea, though, assuming it works: allow the crate keyword as the root of paths in addition to super/self, then lint the shadowing of dependencies and the import of non-dependency root items without a crate-rooted path. The rest is teaching, convention, and rustfmt. The solution to the module confusion is about taking away the requirement for extern crate to be mounted to the local crate’s paths. If you hide their input to absolute paths that start with crate and the legacy format, you get that.


This kind of fallback was also implemented - describes what downsides it has and why later alternatives starting with are better.


One thing that’s hard to account for is syntax highlighting.

It doesn’t speak for a syntax if it’s only readable with syntax highlighting.


I haven’t written much Rust yet, but wanted to share two thoughts:

  • Regarding suffix-:, it somewhat bugs me that the crate name is “less differentiated” than the rest of the path. I feel like if there is such difference, it should be the other way around, i.e. a::b:c:d instead of a:b::c::d. Only in the abstract, of course.

  • Square brackets seem like a suitable visual representation of a “crate”, but maybe that’s just my associations.

For inspiration: