The Great Module Adventure Continues

Hmm this is swaying me to feel about equally happen with std:iter as I would be with :std::iter, since I agree that syntax highlighting would make these very distinct.

I presume crate:module would also be how this works, what about the "dynamic mounts" self and super? Considering that they already work with double colon, and super can be repeated (super::super::module)? I have no presumption, just want to know what you're thinking.

Does any other language use : for this purpose? We chose :: for familiarity. The only language I can think of that doesn’t use : or . is PHP, which used \ to uh, much derision from many people :frowning_face:

I’m reasonably afraid that will happen here too. For this reason, I much prefer :: to :.

15 Likes

It also seems a bit weird that the crate is somehow not like the rest of the path.

I agree, at first it was really confusing when I read that crate and extern were part of the path. I would rather write something like:

use(crate) file_read::for_each_line;

use(extern) {
    regex::Regex,
    std::{
        env,
        process,
        io::{self, Write}
    },
}

// or

use(extern) regex::Regex;
use(extern) std::{
    env,
    process,
    io::{self, Write}
};
3 Likes

@mrborrowck Oh, I like that! Unambiguous prefix at import site, but doesn’t change how they look inline.

Regarding the crate-following :, for some reason I feel like it should be spaced:

use crate: file_read::for_each_line;
use regex: Regex;
use std: {
    env,
    process,
    io::{self, Write}
}

But that’s a little awkward (the space in : {} is a bit jarring), and looks odd as part of type ascription in other places:

let x: std: io::Result<_> = ...;
1 Like

I think there’s ambiguity on stable with let, as in let foo: bar = baz. That is, the following compiles on stable:

mod m {
    pub const X: () = ();
}    

fn main() {
    let m::X = ();
}

By itself, crate_name: syntax seems OK to me, but it does seem slightly inconsistent with : and ::

To me, there’s always been an analogy to “drive volumes” here.

FWIW, I find UNIX's leading / so much more beautiful than the windows convention with drive letters.

1 Like

I think something similar was already proposed in earlier threads, but I would like to propose it again:

// `[]` is a short-cut for `[crate]`
use [] file_read::for_each_line;
// no need to use [..] for relative imports
use super::Foo;
use self::bar::baz;
use [regex] Regex;
use [std] {
    env,
    process,
    io::{self, Write}
};

let x: [std] io::Result<_> = ...;

In let expressions it looks a bit weird, but I think it’s fine, as it will be an incentive to use explicit import.

1 Like

Prefixing a path with double colon (::) in C++ means "look in the global namespace", so using a double colon will get even extra familiarity from C++ folks if it means roughly the same thing in Rust.

1 Like

I think this could be pretty nice if one considers self and super as modules:

// double colon after modules
use self::StatementKind::*;

// single colon after crates:
use crate:mir::StatementKind::*;

use std:collections::BTreeMap;

Syntax highlighters would probably colour the crate part of a path could differently. I quite like the idea of the crate as a "drive volume". It's shorter and more to the point than the noisy, leading ::.


How about using a forward slash after the crate?

use crate/mir::StatementKind::*;

use std/collections::BTreeMap;
// And double colon after modules:
use self::StatementKind::*;

A slash would make use in expressions ambiguous, just like a colon is ambiguous with type ascription.

One reason to be cautious here: consider that we already get confusing error messages due to typos like Box:new. I suppose though that we can probably detect this scenario and be smart about the error we give ("no crate named Box, did you mean to use ::?")

1 Like

(FWIW the options which twiddle bits of non-alphanumeric syntax in paths, like single colon or [foo]:: or such, mostly weird me out. I haven’t developed any strong preferences among the other options.)

4 Likes

I’d like to rearticulate one point: I agree with @matklad that all crates should be at the same level. E.g. ::crate::* and ::regex::*, or whatever option ends up being chosen. crate::* and extern::regex::* put the crates at different levels, which feels wrong to me.

I don’t really care how we get there, but I am very loosely in favor of dropping/deprecating the implicit leading :: in use paths. It being allowed but not having any meaning threw me for a loop initially.

6 Likes

Requiring a leading :: for imports sounds exhausting, but I saw it mentioned upthread, and I really like how it works if we allow bracketing use statements:

use {
  ::std::{
    io::Read,
    io::Write,
  },
  ::crate::root_import,
  my::local_import,
};

// Additional use blocks allowed.
// Braces can be omitted for single imports
use ::absolute::abs_import;

// Read, Write, root_import, local_import, abs_import

Leading :: is fewer characters than use, so absolute imports in this scheme are still as easy to type.

My preference for this form is how discoverable it makes the absolute/relative path rules. It’s a syntax you can easily “twiddle with prefixes” to get things to work. (Fiddling with prefixes is a huge part of how I learned Rust!) And the compiler can easily suggest how to correct your syntax.

Also, there’s little new syntax added. The crate reference is the newest concept, one which is convenient and a feature I’ve opted into. (Requiring extern would be a feature with less obvious upside to opt in to.) Otherwise users actually need to remember less about how paths work than they did before.

Lastly, being able to hoist ::absolute::module::path invocations from the middle of code into the use block verbatim is a nice touch in making the language consistent.

These are some of the upsides I’d be looking for in any new scheme—as little new syntax as is suitable for an epoch, be as easy to type out, and have benefits for users who switch to a new scheme.

Edit: To strengthen the association between a leading :: and a path name, I would not be in favor of letting a to-level :: be grouped, since a tendency would exist to use it and thus make less clear relative vs. crate-level imports:

// Allowing leading :: would make it hard to see the two Write imports differ
use ::{regex, std::io::Write, other_crate};  // use std crate, then its import io::Write
use {std::io::Write}; // local module, but visually looks like above!
2 Likes

All right, this thread seems to have died down, so let me try and take a stab at summarizing what people were saying. Let me know obviously if you feel I left something out!

Also available in a gist, where the formatting is slightly better.

The proposals

Let me first re-summarize in a very short-hand form the set of “major proposals” that I’ve seen.

Name Crate-local use External use External in fn body
Today use foo::bar; use std::cmp::min; ::std::cmp::min
leading-crate use crate::foo::bar; use std::cmp::min; ::std::cmp::min
leading-:: use ::crate::foo::bar; use ::std::cmp::min; ::std::cmp::min
leading-extern use crate::foo::bar; use extern::std::cmp::min; extern::std::cmp::min
leading-: use :crate::foo::bar; use :std::cmp::min; :std::cmp::min
suffix-: use crate:foo::bar; use std:cmp::min; std:cmp::min
[crate] use [crate]::foo::bar; use [std]::cmp::min; [std]::cmp::min
[] use []::foo::bar; use [std]::cmp::min; [std]::cmp::min

Here was the post proposing each variant, and a few other notes:

(There was one proposal I saw but did not include, which is this one by @dhardy. I’m not sure that I fully understood it. I think it is saying that we change was use foo::bar means, but still permitting ::foo::bar to be ambiguous, and hence to be leaning quite heavily on fallback – not just for a transition, but forever?)

How to compare the proposals

Based on the comments made so far, I think there are a number of axes on which we can compare the proposals.

The “1path” property

The first observation is that the proposals can be broken into two major category. Most of the proposals enjoy the 1path property, which basically means that the same paths work in function bodies and use statements. This means that we can potentially – eventually – support use foo::bar with the same meaning as use self::foo::bar.

In order to make this transition, though, we have to disallow use foo::bar as legal syntax for an absolute path by the time the next epoch starts.

Note that we don’t have to actually decide whether to permit relative paths in use statements yet. What we do have to decide is whether or not to clear space to permit us to add it during this epoch. We could decide to defer this until the next epoch, and thus allow the current forms and the new forms to co-exist for longer. (Update: See also the note at the end, “on the cost of transition”.)

Amount of transition

Although it does not have the 1path property, the leading-crate proposal does offer the smallest transition from today’s syntax. This because things like use std::cmp::min still work. All other things being equal, it seems clear that less transition is better.

It seems like we can order the amount of transition as follows:

  • leading-crate:
    • crate-local paths change, but external paths do not
  • leading-:::
    • crate-local paths change
    • external paths in a use change, but to a form that is legal today
  • the others:
    • crate-local paths change
    • external paths change
    • the new form is not legal today

It feels to me like the existence of a functional rustfix probably makes the raw amount of change relatively unimportant. It’s hard for me to weight the “feeling” of adopting the existing ::std::cmp::min paths more broadly versus introducing a new form.

@SimonSapin also makes the point that the precise manner of the transition matters a lot, perhaps more than the amount of change. This relates to the next point (coexistence).

Update: See also the note at the end, “on the cost of transition”.

Coexistence

The proposals that introduce a completely new form (everything but leading-crate and leading-::) co-exist smoothly with existing paths, allowing for a simpler, gradual transition. In particular, we can trivially keep all existing paths working, but simply deprecate those that we don’t want to support.

(Note that if we want to pave the way to 1path, we do want to make use statements require the newer forms in the new epoch. As noted earlier, we may prefer to leave that for another epoch.)

There are two to adopt leading-crate or leading-::. I’ll describe in terms of leading-::, but the same applies to leading-crate. The first is the “flag day” approach:

  • Leading up to the epoch, we require extern crate declarations and we stabilize crate-local paths.
  • We issue a deprecation lint for paths that do not fit into the new model, suggesting that you adopt either use ::crate::foo or use ::std::foo.
  • We also issue a deprecation lint for extern crate declarations that use #[macro_import] or other things, preferring instead to use explicit macro imports.
  • When you opt into the new epoch, those extern crate declarations become dead code, so you can remove them.

This has three downsides:

  • you need to make all the changes before moving to the new epoch;
  • you also have to make change after moving to the epoch;
  • we still require extern crate in the leadup to the epoch.

The other approach is the fallback approach. The feasibility of this approach is not yet known; @petrochenkov has indicated it may be plausible.

  • We enable both sets of paths early.
  • When we see an ambiguous path like use ::regex::foo, we first try to resolve in the old style. If that succeeds, we issue a deprecation lint, but allow it to continue.
    • If that fails, we can use the new semantics.
    • Note that name resolution is actually a more complex feedback process, so determining “success” or “failure” can be tricky.

Aesthetic appeal

And of course there is always aesthetic appeal. To some extent, I this is a matter of “getting used to things”, but it’s importance is also not to be underestimated. If people first coming to Rust see what seems to be “heavy” or “ugly” syntax, they may leave before they have a time to get used to it and see it’s inner beauty. =)

Summary of properties

This table summarizes the proposals in terms of the properties above. It’s hard to judge aesthetics, of course, but I’ll note some of the specific concerns that have been raised.

Name 1path amount-of-transition coexist aesthetics
leading-crate small w/ fallback
leading-:: yes medium w/ fallback
leading-extern yes high yes unbalanced, long in a fn body
leading-: yes high yes
suffix-: yes high yes : vs :: confusing, amb. with type ascription
[crate] yes high yes
[] yes high yes
  • The “long in a fn body” comment for leading-extern refers to the fact that something like impl extern::fmt::Debug for Bar feels quite unwieldy. Example comment.
  • The “unbalanced” comment refers to the way that referencing something from an external path requires an extra segment relative to crate-local paths. Example comment; and another.
  • The ": vs :: confusing" comment refers to @arielb1’s observation that, if we ever achieve 1path, then the distinction between use foo:bar; (bar from crate foo) and use foo::bar; (foo::bar relative to local module) would likely lead to confusion.

Observations from the thread

Re-reading the comments in the thread, here are some observations.

A few other things

There were a few proposals for tweaks on “nested” syntax that might go along with each variant:

// Suffix-`:` proposal
use std: { ... }; 

// Leading-`::` proposal
use {
  ::crate::foo::bar,
  ::std::fmt,
};

Suggestion

I’d like to encourage everyone to actually write code using these proposals. I’m tempted to go and implement leading-: and maybe suffix-: as well; I’ve found that writing even a modest amount of code is very helpful to get a stronger feeling.

Updates

I am updating this post with new proposals and notable bits of new conversation.

On the cost of transition. As discussed in this later comment by @rpjohnst and my response, a move to permit relative paths would “repurpose” existing syntax, which comes at a cost in terms of its effect on existing content in the ecosystem. Even if we want to get there eventually, it is probably wise to move slowly, to give content time to adapt: at the most extreme end, we might begin in this epoch by deprecating the “to be replaced” form, so that you only get warnings. Then, in the next epoch, we might make it a hard error initially, and replace it later on. This a knob we can turn and there was extensive conversation about the tradeoffs in the Epoch RFC itself; it seems like, especially initially, it is probably wise not to “turn too fast”, so as to avoid creating the indelible impression that epochs mean massive churn (which is not the intention).

7 Likes

@nikomatsakis: I’m confused about the “unbalanced” part of the “leading-extern” one. Wouldn’t everything that upholds 1path and distinguishes extern and crate-local paths be unbalanced by that definition?

Another aspect of the “amount of transition” axis is the effect on existing documentation. There is a substantial amount of example code out there on StackOverflow, in blog posts, etc. where it won’t be fixed by rustfix. My guess would be that this kind of stuff primarily uses external paths, both in uses and in expressions, so preserving those would be the least likely to break that example code.

This includes people’s habits- rustfix or no, the more drastic a change the more annoying it will be to get used to, somewhat defeating the purpose of making it to begin with. In previous discussion, it was also pointed out that some people will be maintaining code in both epochs for a while- any crate supporting an old compiler will have to remain in the old style, so maintainers will have to “get used to” the change repeatedly, every time they switch projects.

Further, despite the requirement that crates in different epochs be interoperable, some may even want to maintain code that compiles in both. For example, a crate that supports older compilers but with a feature to opt into something from the new epoch. In addition to placing some more importance on the amount-of-transition axis, this also adds a twist to the coexistence axis. For example, it softens the downsides of the “flag day” approach for crates that would be keeping extern crate around regardless. (On the other hand, at this point the range of compiler versions that will even exist with crate:: but without the new epoch is will probably be small.)

3 Likes

No- leading-::, leading-:, and suffix-: all uphold 1path and distinguish extern from crate-local paths. That is, ::dep::foo/:dep::foo/dep:foo vs ::crate::foo/:crate::foo/crate:foo- all use two segments for a top-level item regardless of whether they're extern or local. Leading-extern, on the other hand, is extern::dep::foo vs crate::foo- three segments for an external crate's top-level item vs two for a local one.


Along the same lines, the inverse of "long in a fn body" applies to leading-:: and leading-:- they're "long in a use." Nested imports supposedly mitigate this, but they were primarily introduced to mitigate extra textual path segments (crate or extern). Rust already has to fight a reputation for being sigil-heavy, and since most (IIRC from the previous discussion) use paths are absolute, I don't think it would be a great idea to introduce more sigils (:: or :, which is also weird) just to make them match the relatively rare case of absolute paths in expressions.

1 Like

I think leading-: also can be ambiguous in certain edge cases? The Great Module Adventure Continues

Probably…