I would like to present an evolution of ideas presented in the comment to the second module proposal. This proposal tries to incorporate ideas from other posts (and respective comments) and to find a reasonable compromise on some issues. It still has some holes in the reasoning and description, but I hope it will help with the further discussion of the topic.
Other proposals and discussions
- Revisiting Rust’s modules
- Revisiting Rust’s modules, part 2
- Revisiting modules, take 3
- pre-RFC: inline mod
- pre-RFC: from crate use item
Abstract model
We can model module system as a way to construct a directed graph (possibly with cycles, see open questions) with two types of nodes: item and module. “Item” type covers not only usual visible items: structs, traits, functions and others, but also “invisible” as well, e.g. impl blocks or trait implementations, which should be linked to the graph to be usable. “Module” type essentially just a way to organize access paths to the items from the given “module” node. (e.g. from the root represented by lib.rs
, which will be named as “crate” from here) Note that one item can be accessed through different paths. (e.g. crate::foo::bar::zoo::Item
and crate::prelude::Item
)
Edges have only one type. All nodes and edges should be organised in a such way that all “item” nodes should be reachable from the “crate” node.
Nodes and edges can be marked as “exported”. Root node is always marked as “exported”. All “exported” nodes and edges should form a connected graph (note: not tree), which will be called “exported” graph.
We can link external “exported” graphs from our graph by creating edges to its nodes.
In the code
To represent the graph described earlier we will need the following tools:
-
use path::to::node;
– main tool for creation of edges (linking). Path is relative to the module in which it’s used. If needed will attempt to create nodes and edges by following files and directories on the file system. Absolute paths represented using reserved node namecrate
which represents “root” node. (usuallylib.rs
, but can be overwritten byCargo.toml
) So absolute import will look likeuse crate::foo::bar;
. -
from cratename import path::to::node;
– create an edge to an external “exported” graph from crate listed in theCargo.toml
dependencies section with absolute path inside of it.path::to::node
should go over nodes and edges marked as “exported”. -
export path::to::node;
– linknode
to the current module and mark it as “exported”. Node can be marked as exported more than once.path
andto
stay unchanged. (so if they were not marked as “exported” they stay unexported) Edgecurrent_module -> node
marked as “exported”.
While it’s enough to have tools described earlier it’s certainly inconvenient to use just them. Thus we need additional tools usable inside mod.rs
and lib.rs
:
-
use(auto) [self];
– will desugar intouse {foo, bar, ...};
wherefoo.rs
,bar.rs
and others are files in the same directory. -
use(auto) foo;
– will link modules defined in the files insidefoo
subdirectory, which should not containmod.rs
. -
use(inline) [self];
– will link all public items inside modules defined by the files in the same directory, but will not link those modules. So “module” node defined by thefoo.rs
will not be reachable from the root, but all public items inside it will be linked to the current “module” node. -
use(inline) foo;
– “inline” files insidefoo
subdirectory, which should not containmod.rs
. -
export(auto/inline) [self, foo];
– behaves exactly likeuse
s, but additionally marks linked nodes and respective edges as “exported”. -
import crate_name;
– link root node of external crate. (analogue ofimport crate_name import self;
)
Dangling “exported” node (i.e. node without path from crate
node passing through nodes only marked as “exported”) will trigger a warning while compiling, as it will not be accessible for user of the crate.
We can “export” nodes (items and modules) imported from another crate too, so this is a correct code: import foo;
Privacy
Only items marked as pub
can be marked as exported
, otherwise it will result in the compilation error. pub
implies pub(crate)
. Stricter privacy constrains can be placed on items which will limit edges creation and referring through paths.
Common patterns
Some common library patterns and how they will look under this proposal. (section will be updated based on discussion)
Facade
export(inline);
fully covers this use-case and additionally allows more flexible directory structure. E.g. using features
example from the first @aturon post we can write:
export(inline) {self, flatten, map, select};
Here flatten
, map
and select
are subdirectories which contain flatten.rs
, flatten_stream.rs
, map.rs
, etc. lib.rs
will contain export future;
Prelude
We have crate with the following items (all path points marked as “exported”): foo::t:Item1
, foo::t2::Item2
, bar::Item3
. For convenience we want to expose them through prelude, so users could import them as from my_crate import prelude::*;
. To implement it in lib.rs
we need to use export prelude;
and in prelude/mod.rs
:
export crate::foo::t1::Item1;
export crate::foo::t2::Item2;
export crate::bar::Item3;
Or if we really want to abuse multi-line:
export crate::{
bar::Item3,
foo::{
t1::Item1,
t2::Item2,
}
}
Re-factoring single file into folder
For example we have foo.rs
containing Item1
and Item2
which became too large. We want to create a folder foo
with the code for those items. We can just move foo.rs
to foo/foo.rs
, create file foo/bar.rs
and copy Item2
code to it. Now inside lib.rs
if we had export foo::{Item1, Item2}
, we can change it to export(inline) foo
, or to export foo::{foo::Item1, bar::item2}
.
Simple crate
lib.rs
:
export foo;
foo.rs
:
export Item;
pub struct Item;
bar/mod.rs
:
export(auto);
bar/zoo.rs
// Will not be accessible as crate::bar::zoo::Item in the exported graph
use crate::foo::Item;
pub struct Zoo;
Item
will be accessible through from simple_crate import foo::Item;
and Zoo
through from simple_crate import bar::zoo::Item;
Pros and cons
Pros
- Clear design with relatively small number of rules
- Explicit implicitness of using folders and files for defining modules.
- Flexibility, allows fully explicit or highly implicit approaches
- Intuitively covers common use-cases
- Easier to teach
- Solves “automatic promotion of key items” problem
- No conflict between simultaneous
lib.rs
andmain.rs
Cons
- New keywords
- Significant breakage compared to the current system
-
auto
andinline
will certainly be most common, which will encourage implicitness across ecosystem - In depth explanation can be a bit tricky due to the more complex model
QA
Why “from foo import bar
” and not “from foo use bar
”?
We already breaking use
pattern with the introduction of from
keyword, so the only reason to keep use
is for backward compatibility. Meanwhile introduction of export
keyword makes import
feel quite natural, while also allowing convenient shortcuts like import cratename;
instead of from cratename import/use self;
. And of course it will feel very familiar to people coming from Python, which one of the main sources of Rust grow.
Don’t we place too much functionality on use
keyword?
Probably yes, in this proposal use
and by extent export
can be use for two things: linking source files from file system and creation of “shortcut” links. Initially I thought about using mod
(plus mod(auto)
and mod(inline)
) for linking files and convenience combination export mod(auto/inline) [foo]
for exporting stuff, while using use
keyword only for “shortcut” edges creation. I like such design a bit more as we explicitly define two types of edges on module graph, with mod
edges required to form a tree, it simplifies some things conceptually, but makes it a bit harder to learn. (beginners confusion between mod
and use
which we witness today) But looking at a general direction of other proposals, I’ve decided to remove mod
as well. Although this decision can change based on discussion.
Open questions
- How to correctly define
super
nodes in the given model. - Rules regarding “bad practices” of using
auto
andinline
. - Can we do without restriction on
mod.rs
for directories linked throughuse(auto/inline) foo
? - Change keyword usage to be more backward compatible?
-
crate::foo::bar
vs::foo::bar
- Should we keep
mod
keyword for example for inline modules? - Should we explicitly forbid creation of cycles in the module graph or should we allow it?