Relative paths in Rust 2018

If you have the following layout:

src
\ main.rs
\ one.rs
\ two.rs

This represents the module hiearchy:

crate
\ one
\ two

Which is evidenced by your main.rs containing mod one; mod two;.

Non-mod.rs mods is that you can do this:

src
\ lib.rs
\ one.rs
\ one
| \ sub.rs
\ two.rs

instead of

src
\ lib.rs
\ one
| \ mod.rs
| \ sub.rs
\ two.rs

for the following module hierarchy:

crate
\ one
| \ sub
\ two

All of the mod xxx; declarations remain. The only difference is that xxx/mod.rs is allowed to be called xxx.rs.

TBH, I think the feature was poorly named. You already have modules from .rs that are not named mod.rs. It's just that those modules were required to be leaf modules previously, rather than being allowed to have submodules.

The privacy barrier is the module. You're suggesting that the contents of src/module/source.rs be in the module crate::module instead of crate::module::source. Thus, it changes privacy.

ahh I missed this. I was thinking that you don't need those mod statements. Thank you. So it is more verbose than what I was thinking :slight_smile:

I was more suggesting contents of src/module/source.rs would be in crate::module if it is not explicitly defined in mod.rs as if all the contents of source.rs is directly in mod.rs

All of these posts from people saying they never use self or super has me worried that somebody at some point is going to propose getting rid of them. So, as a defense mechanism, I feel the need to share my pet pattern:


Most of my modules begin as inline modules, created ad-hoc for the purposes of namespacing and/or reasoning about privacy and invariants.

For instance, say I have this helper type Thing, and I want to implement IntoIterator for it. Often I’ll wrap a mod thing around Thing so that I can use a standard name like Iter:

// a newborn baby module, defined inline (there is no thing.rs)

pub(crate) use self::thing::Thing;
pub(crate) mod thing {
    use super::*;
    
    pub(crate) struct Thing { ... }
    pub(crate) type Iter<'a> = ...;

    impl<'a> IntoIterator for &'a Thing { ... }
}

Here you see:

  • A facade pattern reexport of Thing, using a self path.
    • done here because Thing existed at this location before mod thing
    • I always put the reexport right above the module, like an annotation of sorts. To me, the mod-plus-reexport are a single unit.
  • use super::*;, for precisely the same reasons that many people use it in a tests module.
    • It makes the module cheap to create; nothing is “too small” to deserve one.
    • My imports are still easily found, at the top of the file.

Later, when mod thing has gotten a fair bit meatier and is beginning to take up a lot of space, I upgrade it to its own file:

  • The super::* import is replaced with a full import list (all absolute paths).
  • Facade pattern re-exports remain near mod thing; and continue to use self:: paths.
  • Private imports from self::thing, if any (this is rare), are changed to absolute paths and put in the parent’s import list like any other.

As you can see from the first and third bullets, even I agree that relative paths are not nice for most purposes. For private imports I prefer use path::to::this_mod::thing::foo over use self::thing::foo, because, as I often see it, I am more likely to move thing.rs (to e.g. somewhere nearer the root) than I am to move the entire directory for this_mod.


tl;dr:

  • self:: paths are useful for the facade pattern.

  • self:: paths and super::* are both useful for inline modules.

2 Likes

My initial response to this proposal was somewhat negative, along the same lines as @steveklabnik’s post. In particular, it felt to me like too big of a change to consider so late in the game. But after talking to @aturon, I realized the change was not so big after all. Since I think other people might have similar feelings, I thought I’d write my own explanation of the change, and why its not so big.

The problem being addressed here is, one of the central problems the module changes is trying to solve, is that paths are resolved differently between use statements and local paths. In Rust 2015, the way this works is this:

  • Use statements are resolved from the crate root.
  • Local paths are resolved from the local module.

Despite being simple to explain, this has caused lots of confusion. So the 2018 edition’s current approach looked like this:

  • Use statements are resolved from the “extern crate” scope.
  • Local paths are resolved from the local module, falling back to the extern crate scope.

The implication of course is that in 2018 local paths can’t be used in use statements, practically necessitating self. In other words, the difference between use and local paths is that paths that would start from the local scope are considered errors in use statements.

The proposal in this thread is to make this change:

  • Use statements are resolved from the local module, falling back to the extern crate scope, with an error when you use is a name defined in both contexts.
  • Local paths are resolved from the local module, falling back to the extern crate scope.

This brings us a lot closer to 1path by just reducing the error cases in use statements to the actual potential ambiguities, rather than disallowing all local paths. If this doesn’t introduce a lot of implementation challenges, this does seem worthwhile to consider.

32 Likes

Josh, using std as your example is misleading, since that’s a widely accepted crate name. The problem is that having any local declaration named the same as any crate introduces ambiguity under this proposal. An obvious solution is to provide a dedicated and explicit syntax for absolute paths involving extern crates, e.g. extern::rand::random.

Requiring all absolute paths except extern crates to have a leading identifier is probably the worst of all worlds, since that’s the one case where you really want to be explicit. If you don’t want to have extern:: sprinkled throughout the module, then you could place a use extern::rand at the top of the module, and directly reference rand::random from then on.

Edit: s/explicit::/extern::

3 Likes

6 posts were split to a new topic: Removing/changing the prelude?

Fallbacks? You mean, magic?

2 Likes

In this context “magic” is a negatively charged term, and isn’t even specific enough about what’s wrong with the fallback to respond to such criticism.

5 Likes

True, it is meant to be negative and I think it is right in rejecting the suggestion firmly and outright. I strongly believe it is generically bad to resolve paths with an algorithm that can suddenly go in an entirely different direction depending on some fact you do not know immediately as a programmer. And the outcome would be hard to debug too, esp. if you do not have the source of extern crates on hand. Keep the rules as simple as possible please. If that results in backward incompatibility or challenging implementation, then weigh that against keeping things as they are. I strongly want to prevent we end up in worse place than before by making the language even more complex, for the sake of less important values I see stressed too often, such as:

  • brevity;
  • elegance;
  • backward compatibility;
  • easy implementation.
1 Like

Something I noted on Reddit:

(Modulo glob imports,) In the 2018 modules model, a leaf .rs always has enough information locally in it to determine whether an import is from a crate or a module, without or with allowing local paths in use. This is not true in the current 2015 model.

A 2015 path starts with (a keyword or) a name in scope at the root of your crate. This could be a module or an external crate, you can’t know without checking lib.rs.

In contrast, a 2018 path starts with (a keyword,) a crate name (where crate stands in for the local crate’s name) or in this extension, a locally defined symbol. This means that (modulo glob imports), your non-root .rs contains enough information to determine if an import is from an external crate, a module, or a local symbol, because the path cannot start with a root module and local symbols are local.

Glob imports break this a little, as you can’t know every symbol that is glob imported without knowing the glob import externally, but I’d argue that any glob import importing a snake_case name is questionable style other than a few very specific, well-known identifiers.

That said, what’s complicated about “a path starts with a crate name or a local name, and is an error if that’s ambiguous; here’s how to disambiguate”?

(Note: I am overall neutral on this proposal.)

4 Likes

That is not what is proposed. Ambiguous paths in use are forbidden, so if you get a path wrong, it won't compile, and the compiler will tell you why. I presume it'd be something like:

use foo;
    ^^^ error: you have both module `foo` and crate `foo`, 
        use either `crate::foo` for the module in this crate, 
        or `::foo` for the other crate.

And note that all of these names depend on your code — you control names of your modules, and crates you import (and it's possible to import under an alias).

I don't expect the ambiguity to be a problem in practice, because it's in programmers' self-interest not to give same names to different things :slight_smile:

Even in Servo's case of cookie crate and cookie module, I think that's just unnecessarily shooting oneself in the foot. These could be imported as (I'm guessing here) cookie_storage, cookie_parser, cookie_whatever to avoid ambiguity. Even when the compiler can properly namespace a bunch of things named cookie, it's still needless complication for humans reading the code and having to disambiguate namespaces in their heads.

3 Likes

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.

4 Likes

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

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

@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.

2 Likes

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
};
1 Like

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.

2 Likes

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::*.

@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>

@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::.

@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.

1 Like