Revisiting Rust's modules

Agreed.

However, I don't believe the directory-based proposal will make it any easier- and I worry that it will make it harder, especially given the existence of the current system alongside it. _ also seems a very magical, non-obvious mechanism that's hard to search for, as opposed to pub(crate) which is easily searchable and has analogs in other languages (e.g. internal in C#).

I'm also not convinced that the reform even needs to be primarily a change to how the module system works. Previously I suggested some new compiler messages that might make the current system easier, and others have proposed much smaller tweaks to the current system that address the concerns in the post. We could also take a look at other languages that aren't known for confusing module systems.

For example, Python:

  • A file is a module is a file, much like Rust, but without supporting mod name {}.
    • A directory must have an __init__.py file in order to be a module and contain submodules, much like Rust's mod.rs. Other files in the directory are submodules, just like Rust.
    • __init__.py has to include an __all__ variable for from something import * to bring in any submodules. Submodules not listed can still be brought in explicitly, however.
  • It has two forms for pulling in a module (import something and from something import ...).
    • import something is identical in namespacing behavior to mod something.
    • from something import * is similar to the proposed inline mod; equivalent to the current mod something; use something::*; without bringing something into scope.
  • Both absolute and relative paths are used
    • in imports in they are relative to a list of search paths, much like Rust's list of extern crates and including the main entry point.
    • in regular code they are relative to the current module

Translating this to Rust and the post's issues, we should be able to gain a lot of familiar behavior with a much smaller (and backwards-compatible!) proposal:

  • For directories containing mod.rs (or lib.rs or main.rs), automatically include neighboring files and neighboring dir/mod.rses as private submodules. This preserves single-file modules and still cuts down on declarations.
  • Let mod.rs expand submodules' visibility and assign attributes with explicit pub mod or pub(crate) mod declarations, somewhat like Python's __all__. Error messages for this situation would look like "the module foo::something is private; declare it pub in foo/mod.rs.

For the beginner case, this brings us back from two forms (mod and use) to just one (use). Create another file, put some code in it, and either reference it directly with other_file::function or use it with use self::other_file::function or use self::other_file::*.

We lose protection from accidental extra files, which Python has because it's dynamic and they're never imported, but at least we don't get accidental other directories. We also lose the ability to comment out a module declaration, but we keep something close by renaming its file extension instead (including of mod.rs).

I believe this addresses the learnability and repetition problems without changing things too drastically. extern crate could also help, and inline mod (or with this proposal maybe just use something::*) should help with the facade issue. How does this sound, to proponents of the current system and to new users?

edit: Just read the recent extern crate inference RFC. This seems to line up quite nicely with it, including the concept of adding back in extern crate/mod declarations to add visibility, aliasing, and attributes to them.

8 Likes