This is a third module proposal, very similar to the second one but iterating a bit more. It clarifies a few things from the previous proposal and introduces one additional significant change.
The trouble with “everything is an item”
First, a digression.
In Rust, everything is an item except for let statements and expressions. Every module is just a collection of items (and modules are items), and you can access any item in the crate by naming its path. This means an extern crate
, a use
, a mod
are all actually just items, which create new names in the present path context which alias names defined elsewhere.
This sounds very simple, and from one angle it is. But I would contend its the source of a significant amount of confusion around the module system:
- Users often don’t expect
extern crate rand;
to bringrand
into scope, and are confused about whyrand::random
works in the crate root, but nowhere else. - Users often report confusion about when to use
mod
and when to useuse
; they often feel that mod is a “weirduse
statement” because from their perspective, they’re usingfoo
by declaringmod
(not adding it to their build, and then it happens to be in scope because a mod is an item just like ause
is). -
use
being an item is also not the behavior people expect, usually. They thing of use as bringing names into this scope, but not as “mounting” those names to be visible in other scopes. Here’s an example of the surprising behavior that this creates:
use std::io::{self, Read, Write};
fn foo() -> io::Result<()> {
...
}
mod bar {
use io::{Read, Write}; // NOTE: No `std::`
}
As an advanced Rust user, you might appreciate the real brilliance of having these conceptual unifications, and see a certain aesthetical appeal in the way that use io
arises out of these orthogonal rules. I certainly do. But as a user trying to figure out why I have to use std::io;
in the crate root, and I can just use io;
everywhere else (because I’ve stumbled upon that discovery by mistake), this is not a good UX.
I also know there are some users who don’t just appreciate this unification, but consider it extremely important. I’d be happy to hear this position more fully articulated - I haven’t received a clear impression of why this would be so important. But I’d also encourage users to keep an open mind to a system in which:
-
use
is not an item; it brings a name into scope, but does not mount it in the module hierarchy at this location. It does not have a visibility attribute (nopub use
) - A new keyword,
export
, is an item, and does take a visibility attribute.
That’s the biggest new idea of the proposal, but now into the full details of this proposal.
The proposal
Absolute path hierarchy
The absolute root of the path graph, from which paths beginning ::
traverse, is not the module of main.rs
or lib.rs
, but a module which contains these symbols:
- One module for each crate passed by
--extern
, which contain that crate’s module tree underneath it (that is::std
,::serde
, etc) - A special
crate
module, which contains this crate (from the previousroot
module).
That is, absolute paths are written ::std::iter::Iterator;
to get something from another crate, and ::crate::module::Item;
to get something from this crate.
Use statements
use
statements are no longer items, and do not mount names, they only import them. That is, items imported with use
cannot be accessed from other modules.
use
statements take absolute paths beginning at the ::crate::
module, not the true root. So you have:
use module::Item; // an item defined in src/module.rs
use ::std::iter::Iterator; // an item defined in another crate
use self::child::Item; // an item defined in a child module
use super::sibling::Item; // an item defined in a sibling module
Only thing that’s changed in this syntax is ::
on crate imports (and some sugar will make that better soon).
Because use
statements are not items, they do not have their own visibility. Instead, we have a new kind of statement called export statements.
Export statements
export
statements work a lot like use
statements do today - they both mount and import names, impacting the resolution of relative paths in this module and absolute paths in other modules. There are a few key differences from use statements, designed to improve user experience.
First (and most importantly), the path they take is relative to the current module, not the crate. So you are replacing:
pub use self::child::Item;
// replaced with
pub export child::Item;
However, they can still take absolue paths with ::
, just like use statements can (and again, the next section has syntactic sugar to make this nicer).
pub export ::crate::module::Item;
pub export ::std::iter::Iterator;
pub export super::sibling::Item;
pub export self::child::Item; // equivalent to not having self::
The last big difference is that export
is pub(crate)
by default. However, export can take any visibility scope, including pub(self)
if you really only want to make this item visible in submodules.
The from syntactic sugar
To avoid having to write ::
in use and export statements, we also add the from
syntax. from
takes a single ident, which is any of these:
- The name of one of the
--extern
crates, includingstd
. - The speical symbols
crate
,self
, andsuper
.
Both use
and export
statements can be prefixed with from
, which desugars them to taking the appropriate absolute path:
from std use iter::Iterator;
from super use sibling::Item;
from tokio export Service;
from crate pub export module::Item;
We could also possibly support a more complex syntactic sugar that takes multiple paths, but I haven’t worked through it fully:
from std use {
cmp::{min, max},
collections::HashMap,
iter::once,
thread,
}
Modules
Mod statements would no longer be necessary to pick up a file a new file in the crate. Instead, rustc would walk the files it knows to walk (see next section for more info), and mount a module tree from that, possibly before parsing any Rust code.
Files mounted this way would have a pub(crate)
visibility, if you wish to change that publicity, add an export statement to their parent.
pub export submodule1;
pub(self) export submodule2; // if you are very concerned about using something
// from this submodule elsewhere in the crate
Though the names of modules are mounted automatically, they are not imported into their parent, and so they are not visible to relative paths from their parent unless they are imported with use
or export
. That is, you cannot use the name of a submodule without somhow bringing it into scope through use or export statements.
Modules of the form mod foo { /* code */ }
would still exist, with no change to their semantics.
File lists
rustc will only automatically load modules that are listed through two command line arguments it receives: --load-files
and --ignore-files
. The files that it loads are all the files matching load-files, subtracting those which match ignore-files. Both take a list of filenames, supporting basic standard glob expansions.
- When building a
lib.rs
ormain.rs
, cargo tells rustc to load any files matchingsrc/**/*.rs
and to ignore any matchingsrc/bin/**/*.rs
(that is, all.rs
files in thesrc
directory, except those in thebin
directory). - When building a binary in the root of the
bin
directory, cargo tells rustc to load every file in thebin
directory. - When building a binary in a subdirectory of
bin
, cargo tells rustc to load every file in that directory.
This introduces a firmer convention around multi-crate packages. In my survey, most multi-crate packages already conformed to this, though a sizable minority did not (mainly by having both main.rs
and lib.rs
in the same directory):
- For packages with 1 binary and 1 library, the bin should be in
src/bin
while the library is insrc/lib.rs
.src/main.rs
is only used by packages without a library. - For packages with multiple binaries, they should each sit in their own subdirectory of
bin
, none should sit in thebin
directory directly.
However, some users will probably want to opt out of these defaults for multiple reasons:
- They do not want rustc using glob imports to import all of the files in their
src
directory. - They do not want to structure their multi-crate packages in the same way.
For this reason, cargo
will expose a way to control these arguments to rustc directly, probably through the Cargo.toml
. My hope is that most users won’t do this, though, and it will result in a more consistent package structure across the ecosystem.