Relative paths in Rust 2018


#81

Personally, I like the idea of using extern to access external crates. I imagine this could be added backwards-compatibly in the future if desired, so not urgent or a big deal. But I’m thinking from a standpoint of “How do we want to be able to make imports look, with respect to clarity and readability?” And IMO being able to do something along the lines of this:

use std::{
    collections::HashMap,
    io::BufReader,
    // Other std imports
};

use extern::{
    rand::thread_rng,
    regex::Regex,
    // Other extern crate imports
}

use crate::{
    foo::do_a_thing,
    // Other root crate imports
};

use my_submodule::do_another_thing;
// Other relative imports

Would be really nice, from a clarity standpoint. I already essentially organize things in this order (first std, then external crates, then local crate, then relative imports), but being able to make it explicit in a succinct way would be really nice!

Without extern the external crates part ends up like this:

use ::{
    rand::thread_rng,
    // Other extern crate imports
}

Which isn’t the worst thing in the world, but looks a little weird, isn’t (probably) as clear to newcomers, and breaks the rhythm.

Anyway, maybe this isn’t the right place to post this–it’s certainly not urgent! But just my two cents. Over-all, I really like to new module proposal, though I haven’t personally played around with it yet.


#82

std is an external crate, and so would be lumped under extern::.

You can also write use { top, level, stuff } without the leading ::{.


#83

@rpjohnst I thought under this proposal std is also accessible as just std, along with other external crates? I realize that it’s an external crate from a technical standpoint, and therefore I could put it under my use extern::{} block. But std is also distinct from a non-technical standpoint, so I like to organize it as its own thing in my imports.

Regarding use { top, level, stuff }, that’s moving even further away from what I’m trying to get at, I’m pretty sure? Unless I’m misunderstanding you. But I’m looking at ways to make the distinct conceptual sources of imports as clear as possible at-a-glance. So sans-extern-namespace, I would likely do use ::{ ... }, just to make the distinction a bit more visually clear. It doesn’t look as pretty, but it communicates the intent a little better IMO.

I definitely like the “one path” stuff, because it helps make things more consistent. But what I’m getting at here is “How do we use one-path effectively, to write clear, easy-to-understand code?” And I suspect that “lump everything together now, just because we can” isn’t quite the right answer. Rather, “Which things can we lump together–and in what ways–to maximize clear communication and hackability?” is what I’m exploring here.


#84

Oh, I was under the impression your sample there was from a hypothetical scenario where you always used extern:: to access external crates. And the use { a, b, c } syntax was assuming you didn’t like the look of ::{.

If what you really want is a uniform some_place::{, you could do something like this instead:

use std::{
    collections::HashMap,
    io::BufReader,
    // other std imports
};

use rand::{
    thread_rng,
    // other rand imports
};

use regex::{
    Regex,
    // other regex imports
};

use crate::{
    foo::do_a_thing,
    // other crate imports
};

#85

Yeah, that would certainly work too! Personally, I tend to think of things in terms of “std, extern crates, and my crate”, but I realize that’s not necessarily how everyone organizes things in their heads. :slight_smile:

But it would be nice to be able to do that. As I said before, though, it’s not actually a big deal, just a minor wish! And it would be easy to add later, so not actually a critique of this proposal.


#86

That’s fine if you didn’t want to have a module named rand or regexp, or any other crate name that you might eventually want to use (even if you don’t currently know it exists). If, however, you want to name your modules without worrying about what the internet is doing, then it would be preferable for absolute paths to extern crates to be explicitly marked as such, e.g. use extern::rand::*.


#87

@cessen I think rpjohnst’s point is more that for your example to compile, unless we made std more special (its already special of course, but we’ve been moving toward it being less special over time), you would want to have written

use extern::std::{
    collections::HashMap,
    io::BufReader,
}

use extern::{
    rand::thread_rng,
    regex::Regex,
}

//... etc

I also group my imports the way you do, with one difference: I personally prefer for each line to begin with a use statement. This make the use statements have a different visual shape from non-use statements like structs, enums, and functions; in examples like yours, they have the whole opening line, indented lines, closing brace structure that regular code has.

Giving them a different structure names it easier for me to tell where the imports end and the code begins, which makes it easier to quickly find where I want to start reading.

The reason I’m concerned about ideas like extern:: is that its more onerous when you have to type it on every line:

use extern::std::collections::HashMap;
use extern::std::io::BufReader;

use extern::rand::thread_rng;
use extern::regex::Regex;

This becomes an even bigger problem when you want to use a name directly just once without adding it to the whole module as a use statement, as in fn foo<T: extern::serde::Serialze>


#88

@scottjmaddox In the currently-implemented system, that doesn’t actually matter- use statements are always absolute, so no module name will ever cause a conflict. And in the system proposed in this thread, the only modules that might conflict are the ones defined in the current file, in which case you can still resolve the conflict by prefixing the path with :: or self::- no need for the long-form extern::.


#89

@withoutboats

Yeah, I was assuming std would be considered special in this case (or rather, already use-d in the prelude).

In the end, I’m really just bike-shedding here. My personal sensibilities lean towards limiting the non-local names in scope to a known few, rather than a list that grows arbitrarily with included crates. It makes things feel predictable and well defined to me. So grouping crates under extern appeals to me for that reason as well. But as far as I can figure out, the actual practical impacts one way or the other are minimal if not non-existent, so I’m not actually too concerned with whether it matches my personal sensibilities or not. :slight_smile:

What I definitely do like is the unified name system, and I certainly wouldn’t want to block that on whether or not crates are grouped under extern.


#90

I really like matklad’s suggestion:

This seems to be better than both the Rust 2018 main proposal and the OP proposal:

  • It introduces true uniformity to paths in use statements and the rest of the code. I’ve seen people that stumbled upon the special handling of paths in use while learning Rust and they were very confused about it. I think the uniformity is not just a small convenience but an important ergonomics factor.
  • It allows to reason about the code locally, which seems to be one of Rust’s main principles. By looking at the path, you can immediately see what it means.
  • It doesn’t seem to introduce any additional complexity compared to other proposals.

#91

Agreed. The downside is that having :: on the start of paths looks a bit ugly. But I can get over that for the sake of having a simple, logical system.


#92

Another IDE argument for leading ::, inspired by the recent @withoutboats blog post. In the current 2018 implementation, if IDE sees a hex::encode and hex is an unresolved name, it can’t offer precise suggestion, because, for example, you might have intended to add local module with the name hex. With ::hex, IDE precisely knows that you’ve meant an extern crate, and can offer that suggestion with a high priority, using UI for fixes which you surely must apply (as opposed to UI for potential fixes).


#93

As far as I can tell, the only difference between that proposal and the one that was originally proposed here is that, under your proposal, crate names are not brought into scope in every module automatically.

A lot of people have argued that leading :: is ugly, and I’m wholly inclined to agree. It would add a lot of syntactic noise to the use of absolute paths, especially in items. Already, users very much dislike having to write, say impl ::std::fmt::Display for T. This proposal does not fix this ergonomics problem.

To me, the original proposal suggested makes a lot of sense. As far as ambiguity goes, I think there are two possible issues. The first is an example like that provided by @nikomatsakis where macro expansion can change the meaning of other code. I would suggest the following resolution: the leading component of a path cannot be the name of a macro, if that macro shadows another identifier (including a crate name). Thus, cases of mutual dependency will fall afoul of this, as will the following:

struct S;
fn foo() {
    declare_s!(); // declares a type named S.
    let s = S{};
}

Second, there is the case of ambiguity where the compiler is perfectly capable of resolving the situation, but humans reading the code are not. Consequently, the meaning of code may change by the introduction of a name within a glob. These are also problematic, of course, because they lead to great difficulty understanding code. Consequently, the same rule would apply to identifiers brought in by glob: if a name brought in by a glob shadows another name, then it may not be used as the leading component of a path. This can be fixed by a path from the glob’s parent; if needed the author should make the import use foo::{self, *} so as to be able to refer to foo in paths. This would be a breaking change, but easily

These rules would allow users to refer to names imported by glob or declared by macro, provided that they are careful to qualify references to shadowed entities. If they do not, then the moment they add one, the compiler will give an error and force the discipline to be kept. Nothing will force the user to change immediately, however, so harmless globs will not lose their ergonomics, and any collisions are easily fixed.

For the most part, these transformations could be done automatically in existing code. In case of naming collision between multiple glob imports (let’s not talk about macro_use), situations where the conflict is between two conflicting globs, we could require Cargo to add a self documents, so maybe not the best choice.


#94

I would really like to dig deeper into why people consider leading :: ugly, because I personally can’t really understand this sentiment.

  • on the writing side of things, it’s only a two characters long, and it’s very easy to type.
  • on the aesthetics side of things, it’s also only two characters long, fits well with usage of :: as a components separator, and mirrors the syntax of file systems paths like /abs/path and rel/path.
  • on the readability side of things, it gives an important information of where the name comes from. It maintains the property that for any identifier in the file, it is either possible to immediately tell where it comes from, or use Ctrl+F inside the file to find the definition.
  • Finally, if you find :: ugly, than introducing crate:: should feel ugly to you as well, as it is even longer than ::.

#95

Yes, I too am perplexed with the sentiment that it is better to have ambiguous paths than to have a leading “this is an absolute path absolutely, no ifs , ands, or buts” indicator (like every other “path”). I really do not understand this line of reasoning at all. I think it is way too focused on avoiding a couple of characters of typing at the high cost of ambiguity that must have special rules to disambiguate.


#96

I can think of a few reasons :: could be ugly. First, it is composed entirely of punctuation. As leading punctuation, it’s suspiciously close to a sigil, though it isn’t. Perhaps more importantly, if you don’t know what it is, you can’t google it. Second, colons are interstitial punctuation–they divide things–and thus it’s unsettling that a colon would begin a path.


#97

I don’t believe everything can always be broken down into a concrete and easy to understand “why,” particularly when aesthetics are involved. I, personally, regularly avoid (although not always) the use of a leading :: because I find it to be displeasing. I would, on the other hand, not mind using crate::. I can’t really break it down more than this. It’s just an aesthetic preference.

N.B. This is kind of a drive by comment, because I haven’t read this entire discussion. FWIW, I don’t really mind the status quo, so if whatever it is that’s being proposed doesn’t somehow increase the frequency of :: beyond what the status quo does, then I don’t have any strong complaints.


#98

I agree with you that its perplexing how disliked it is, but I also agree with @burntsushi that I really strongly want to avoid using leading ::. I think @illustrious-you is on to something about “interstitial punctuation” and feeling like a “sigil.” It definitely is not about the two character count.

I’m also having a little trouble understanding the use case you’re describing (admitting that I don’t use autocomplete). If the name hex is not in scope, is it so much worse to make both suggestions (either you need a mod hex; or you need to add an extern dependency)?


#99

I think @illustrious-you is on to something about “interstitial punctuation” and feeling like a “sigil.”

No one dislikes the syntax of absolute paths, and / and : are both interstitial :slight_smile: So looks like it’s indeed a purely aesthetics preference (which is a totally valid reason to dislike a particular syntax)!

I’m also having a little trouble understanding the use case you’re describing (admitting that I don’t use autocomplete). If the name hex is not in scope, is it so much worse to make both suggestions (either you need a mod hex; or you need to add an extern dependency)?

Note that IDE arguments are much weaker than "violation of Ctrl+F". The IDE can provide slightly more fluent experience with :: instead of extern crates in prelude, but the difference is not dramatic dramatic.

Case 1, autoimport. You’ve already written [::]hex::encode, IDE knows that hex name is unresolved. With prelude, there are various possible fixed for the problem

  • create a hex submodule in the current module.
  • import hex module from some existing crate.
  • add hex = "1.0.0" to Cargo.toml.

So the idea has to show a “here’s a problem” light bulb, the user will have to invoke Alt-Enter once to see a list of possible fixes, and then select and apply the fix. With the ::, IDE knows that it’s about extern crate, so the user does not need to select a fix, there’s only one available.

Case 2, autocompletion. You’ve just written [::]he and IDE shows an autocomplete popup. With prelude, it shows all the local names which match he, plus dependencies. One problem here is, even when you type a local name, IDE needs to suggest extern crate names. Another problem is that IDE can’t suggest external crates which are not already dependencies (It might, but that’ll make the common case of local variable completion extremely noise). In contrast, if you type ::he, IDE can show only existing dependencies, plus other crates from crates.io which will be added to Cargo.toml automatically when the corresponding completion option is selected.

Let’s also describe a Ctrl+F property in a similar style.

First, let’s see what happens if extern crates are not added to the prelude.

  • Case 1, :: path. I read the code and see ::hex::enocde. “Oh, it’s an extern crate” I see immediately.
  • Case 2, use hex at the top of the file. I read the code and see hex::encode. “I wonder what this hex is?” I think, type /hex, first hit is the use at the top of the file, and I understand that it’s an extern crate.

Now, what happens if hex is in prelude? I see a hex::encode. I /hex, and hit this very usage as a first occurrence. “This might be an extern crate” I think. I use n several times to make sure it is not a local module declared at the bottom of the file, so I circle back to to the first occurrence. Now I am rather confident that it’s an external crate, but, to be sure, I glance at the top of the file to see if there are any * imports.


#100

Another drive by comment.

As much as I appreciate incredibly powerful IDEs, I think that there is something to be said about the aesthetic drawbacks of leading ::.

I think one of the reasons Python is so popular is because of how elegant and simple it looks to a developer. It’s module system has a few of it’s own problems, but for 99% of the time it works and looks really clean. This isn’t to say that some of Python’s outward simplicity doesn’t have drawbacks (mutable by default, etc), but clearly Python did something right, and I think a big part is aesthetics.