Revisiting modules, take 3

I didn’t respond to your comments earlier (my apologies), but I hadn’t been drawing a distinction between crate root and extern in my earlier analysis, and your numbers confirmed what I had believed, which was that self:: prefixed imports are a very small minority (on the order of 5% or less) of use statements, especially non-pub use statements. Between this and the fact that so many mainstream languages use absolute paths in imports (& I don’t actually know one that doesn’t), it seems clear that making self:: the default would be the wrong default.

2 Likes

That's why I think that a combination of self::LOCAL/super::WHATEVER/::ABSOLUTE/[crate]::EXTERN is the best solution (with unprefixed paths in uses being deprecated).

1 Like

Do those numbers also include use statements that are absolute but could actually be relative?

I've often written use statements as they were relative, then found out that it does not work and used absolute paths instead because I didn't remember that self:: existed. So my code actually contains more absolute imports than necessary and only very few uses of self::.

2 Likes

However, Python also doesn't allow you to use things from another "crate" at all without importing, right? Paths inside the module are relative, and there is no "be absolute" modifier. That would be problematic in Rust at least for error messages, which may have to mention types that have not been imported but only come up due to inference.

Ah, I never thought about this. I didn't even notice that relative imports work "differently" in __init__ vs. other files. Neat!


So do you mean to rule out code like this, or to somehow make it less surprising that this stops working when moved into a submodule?

mod foo {
  fn bar() {}
}
use foo::bar;

fn main() {
  bar();
}

At this point we are talking about use mystuff vs. use ::mystuff, so the question is whether it is worth to have two additional characters at almost all crate-local uses to remind us that these are absolute. Or, phrased the other way around, is some weaker form of path confusion a price worth paying for saving two characters per crate-local use?


This actually would be a special case -- the [std]:: prefix roots the path, but so far I have not seen as [std] as being an item on its own.

Just a small note to keep everybody in the loop:

  • I’ve been talking some out of band with @withoutboats to sort through some of the issues I raised earlier in the thread, and to try to get on the same page with respect to a few knobs.
  • Unsurprisingly, these conversations have led to yet further variants!
  • At this point, where I think we’ve converged (relatively) on the broad outlines, I think the right next step is to write up the latest variant as a full-fledged RFC and move discussion there, where we can all work through the fine details together. This RFC should take into account the points that have been raised throughout the discussion.

@withoutboats is traveling at the moment, but is working on the writeup and will hopefully get it posted toward the end of this week.

Please note, this is not intended to cut off discussion, but rather to signal that this conversation has produced what seems like a viable candidate, and so we want to start the more formal RFC process.

2 Likes

There have been quite a bit of discussions regarding modules here, so I’m sorry if couldn’t follow all ones and someone already had the same idea.

Personally I’m more in favour of only having absolute paths for use. I also hadn’t that much issues identifying external crates by just the first part of use (e.g. use clap::...).

I can see how this gets harder for foreign projects to distinguish between modules from external crates and internal modules.

What if the first part of use always had to be a crate name? So even in the case of importing internal modules you would’ve to prefix use with the name of the crate.

We can't do this because you can have a crate that has dependency on another crate with the same name. For example, the binary cargo depends on the library called cargo. However, what we've decided to propose for the RFC is something essentially the same, except that the current crate is always called crate instead of its name. So like:

use std::io;
use crate::module;
::std::io 
::crate::module

This was neither @aturon's nor my favorite solutions (our favorite solutions differed significantly), but as we sat with it we realized it had a lot of pretty clear benefits and was sort of the pragmatic choice. More justifications forthcoming in the RFC in a few days.

1 Like

So no [std]::io? Well I guess I'm looking forward to the further justification then.^^

4 Likes

To my eyes, pub in pub export seems redundant. In my opinion,

export path::name;

should export name as public by default (without an alternate way of writing that), and the more restricted access levels should be specified using a parameter to export like:

export(self) path::name;
2 Likes

Yeah, the connotations of “export” are suggestive, but potentially misleading. “Export” implies you’re taking something from inside the module and making it available outside the module. Having to say pub export to make it actually available outside the module seems like saying “yeah, but now really do it”. On the other hand, saying export(self) seems like saying “export, but never mind”. I suppose pub(restricted) already suffers from this problem in general, though.

What export is actually doing is bringing a name into the module as an item (as opposed to just useing it in the module). Items declared in the module are already “exported” (even if they’re not public). So it seems like what you’re really doing is importing, though if Rust had both import and use, there’d be no end of confusion about which to use when. “Include” also has more appropriate connotations, but again, people would get confused about when to include and when to use.

“Export” might be close enough, though, as long as you liberally interpret “inside” the module to include submodules, and “outside” the module to… also include submodules. I almost want to suggest—as an alternative to mean bringing a name into the module as an item rather than just useing it—a term which came up near the beginning of this saga: mount. Having mount be visible only to submodules but pub mount be visible publicly would actually make sense, and there’s no risk that people will associate it with the equivalent of use in another language.

Whether you’d be able to get away with using module relative rather than absolute paths is unclear though. The one benefit of export implying taking from inside the module (when it really needs to take from anywhere but inside the module) is that it at least makes the fact that it takes module relative paths self evident.

Edit: I’ve talked myself out of the following train of thought, but I’ve left it in for posterity:

Another term that has very close to the right meaning is “alias”. It would also justify using module relative paths instead of absolute paths. The only thing that might be confusing is that people might expect it to be

[ visibility ] "alias" ident "=" path ";"

whereas here we would want it to be

[ visibility ] "alias" path [ "as" ident ] ";"

The other issue I can foresee is people not expecting alias to be an item, and thus not seeing the distinction from use. In particular, the ident in use path as ident would also be referred to as an alias, potentially confusing people. Whereas mount doesn’t have this problem, since telling people you can use something under an alias or mount it under an alias is relatively easy to understand. Also, “mount” has a very clear connotation of installing something for display, so it would never be mistaken as not being an item.

Not to tease things, but the final RFC is closer to that than the current syntax. :slight_smile:

Planning to work on the RFC through the weekend and post it on Monday morning.

4 Likes

Hmm… of course, you wouldn’t want to tease things. :wink:

Not to be guessing at things, but if “that” is referring to export(restricted), then maybe the RFC will be for pub(restricted) path::to::item. I await Monday morning (or whenever it’s finished) with bated breath.

1 Like

The suspense is killing me.

I love this suggestion! mount conveys the idea that "this is an item" quite well, and pub mount expresses that the mount point is visible to the outside. It also solves the other problem with "export" as well, namely that many items are going to be publicly visible without an export -- whereas the presence of "export" may indicate to people that stuff has to be exported to be usable from the outside.

It seems that mount is better than export in pretty much any dimension.

I wonder if, once we have mount (behaving pretty much like the current use), we even still need use...

The point of use is to… use… things, rather than creating a new path for them in the module hierarchy. One of the core reasons why there’s a desire to do away with “use is an item” is because it’s surprising that, just because the crate is using some external item, submodules now have a new path they can use them from. I think that use and mount (or whatever it’s called, pub if my guess is right) are distinct enough in their connotations and use cases that there shouldn’t be any confusion.

1 Like

That's until you want use super::* or something similar and it doesn't work because imports are not items.

In the first approximation, use does "use" things (i.e. brings them in scope).
It's not necessary to know about the "mounting" behavior, until you actually need it, it's not shoved in your face.

1 Like

That’s until you want use super::* or something similar and it doesn’t work because imports are not items.

That's sort of the point. If you're just trying to use something in a child module as well as in a parent module, it's clearer to use it based on its canonical path in both instances.

There are a couple reasons for not doing this. You might want to create an internal facade for an external library, so you can switch out the dependency in one location rather than spreading the dependency over a bunch of files in your crate. This is best served by being explicit and creating a module whose sole purpose is to explicitly mount all the names you need to use, and have other modules use this facade module. In which case there's no value in having this piggyback on use syntax.

Another reason is you might have a set of external items you need to use throughout your crate, and you don't want to repeat all the use statements everywhere. Again, this is better served by creating an explicit prelude module which mounts all the names you need, and have the rest of your modules do a wildcard use to bring those names into scope. Again, there's no value in having this piggyback on the use syntax, since you don't need every module that uses these items to become a new source to use them from.

I'm not terribly invested in this, but the discussion so far has convinced me that I'd prefer if use was just for using something within the current module, and to have a separate syntax to bring things into a module as items which can then be used by other modules.

Edit: Another argument against use super::* to grab uses is that it's fragile. If you end up no longer needing a particular use in a parent module, now you can't remove it without potentially breaking child modules. In a large codebase, the uses that are in scope might be spread across a large number of files, and knowing where a particular item is coming from can be difficult to identify. Someone who does use super::* because the code is closely related to what's in the supermodule might not even realize all the items they're bringing in, and potentially end up with name conflicts due to incompatible traits, or not noticing that they haven't explicitly used an external item that was silently brought in by the parent. This really seems like an anti-pattern to me.

Its still morning somewhere: https://github.com/rust-lang/rfcs/pull/2108

1 Like

It is not permissible to use this mechanism to re-export something at a greater visibility than its explicitly declared visibility.

// This is an error: you have exported something which is only marked `pub`

What is the rationale for enforcing this rule? It seems to me like an artificial restriction. I can see situations where you want to, say, have a pub(crate) module and then re-export all of them to the outside world elsewhere.

1 Like

You can have export items in a pub module and then re-export the items from anywhere in the crate that can reach the module.

1 Like