Revisiting Rust's modules

I'm against almost every idea brought up in the proposal. As for the issues listed, I believe some of them don't apply, while others should be fixed in different ways. I agree with one issue though, which is that code in lib.rs and main.rs can use absolute paths without rooting them with a preceeding ::.

Implicit mod and crate

The blog post proposes implicit mod and crate, calling the explicit notation boilerplate. I think adding the ability to omit these two statements is bad for several reasons:

  • The Cargo.toml format is no way trivial and easy. There are conditional dependencies, dev-dependencies, build-dependencies, and so on. How should a beginner coming to a new codebase know which crates they can use in build.rs or lib.rs or src/bin/binary.rs if not through the extern crate declarations? Having an extern crate gcc; in build.rs is a net increase in clarity. Rust will be less learnable through this.
  • If you see some code do foo::bar::baz() or even use foo;, where will you know from what foo is? Is it a module? You'd have to check the file system hierarchy. Is it an external crate? You'd have to check Cargo.toml. Is it an inline module? You'd have to scan the file. With current Rust, everything is declared in lib.rs/main.rs either via mod or extern crate so you only have to look at this single file, and the file your statement is in. Of course if you have sth like use super::baz::bar; or similar, you'll know you have to look there. I call this feature self-contained-ness of .rs files in current Rust, and I believe it should be a goal to preserve it. The only exception to this self-contained-ness is macros, which will hopefully be fixed soon. Therefore, the proposal means a clarity decrease.
  • You need to open Cargo.toml. When looking at some crate, I usually don't open Cargo.toml and open lib.rs instead because it contains far more useful info. I open Cargo.toml only to get the actual version of a dependency. In fact, it'd be far more prefferable to have Cargo.toml implicit, meaning that version info and stuff like that is declared next to the extern crate definition. @ubsan has been proposing this and I support it!
  • You need to obtain a list of files. My editing workflow right now is to have a list of open files with content I'm interested in. When I've worked on the compiler in the past, I've had to open tens of files from various crates with definitions and code. Now the rustc project overall contains hundreds if not thousands of files. I don't know which way your editor is displaying stuff to you, but for me who uses Kate, I have the choice between two modes, one is "collapse directories, but display everything a directory contains if its uncollapsed" and the second is "only show opened files". I doubt other editors offer any other or better mode. Let's assume I want to add a built in macro to Rust, so I'd be interested in the content of two files, src/libsyntax/ext/source_util.rs and src/libsyntax_ext/lib.rs (I'll probably also have to look at more files but this is the two I must edit). See the screenshots at the bottom of this post for an overview of the modes. You'll notice that the "show everything in the file hierarchy" spectacularly fails for the rustc usecase. rustc is simply too big! The same problem manifests for smaller projects as well. The big difference to mod declarations is that while you need to do scrolling for such as well, all files that are not interesting to you are simply not displayed, so you can focus your attention on the actual stuff your current focus is on. So ergonomically, it'd certainly hurt me to not have the mod notation for all modules.
  • Don't forget that the percentage mod and extern crate is for most crates in the single digit range and maybe even less than a percent. The general overhead of typing this should be very low.

Summarizing, implicit mod and crate will make the situation worse for clarity, learnability, and ergonomics, while its overhead is low.

Re: Proposal: directories determine modules

  • The proposal seems to add boilerplate, not remove it. Yes, you'll have to type more if you have an util module with a bunch of declarations you want to be pub(crate) because you must repeat the pub(crate) for each single declaration, you can't just say pub(crate) mod foo; and have the declarations inside be pub.
  • leading _ is ugly as hell.
  • It introduces an inconsistency with normal identifier names. Right now pub mod foo and pub struct Bar are highly similar. In the future you will be able to declare a module _foo but calling a struct _Bar would mean nothing. This makes Rust less consistent. Even if you supported struct _Bar to mean pub(crate) struct Bar it would be less learnable as there is one additional way of calling stuff, and highly inconvenient as you can't just grep for struct Bar any more if you wanted to find the definition of Bar.

The seems to want to remove two properties of current Rust that the blog sees as problem:

  • Widespread use of the facade pattern
  • pub having different meanings in different places

First, let me admit that the widespread use of the facade pattern is not the most beautiful part of Rust, but I'm not sure its so bad that turning over the entire module system is neccessary. If lowering its use is really such a big goal, I'd prefer to have the proposal by @ahmedcharles : inline mod. Not sure how it is going to handle namespacing, e.g. does an use foo inside one of the inline mods affect the parent mod, or not... I'd personally prefer that it doesn't affect the parent module.

Second, with inline mod we'll have less uses of reexports, so pub will mean pub(world) more often. Making it mean pub(world) in every case would be wrong IMO as this makes creating module-scoped modules highly inconvenient, and even modules with mostly pub(crate) items, because every time you'd have to type the pub(crate). Instead, let me make an alternative proposal:

Explicit pub(world) and pub attribute for modules

Initial note: I'm disregarding any backwards compat here as well, and build on the assumption that this will be included into a new epoch.

The first part of the proposal is to add an explicit pub(world) (please bikeshed the name if you find a better one) which means that the item is visible to the world.

The second part is that we error if any pub(X) doesn't end up at publicity level X, when its inside a private module and not pub re-exported.

The third (and main) part, making the meaning of pub a module local property:

The main issue with "pub can mean different things" is finding out what it actually means in the end. If finding out is made easy, changing the meaning of pub shouldn't be that bad, should it? Therefore, I propose a different mechanism to the current one: that you can put an attribute #![pub = pub(...)] to a module where you can choose the ... to mean anything from crate to world to super or even in path if you want. All the uses of pub in the module would mean pub(...) like declared in the attribute.

Features:

  • It should default to world, so in most places pub something would mean pub(world) something.
  • Attributes of any super module won't affect any sub module.
  • The places where deeper hierarchies are wished are still very nicely supported as you can just put one attribute per file.
  • Any pub(world) item which doesn't end up in the public API either through reexports or through the module hierarchy should give an error.

Advantages:

  • It would fix the "finding out the meaning of pub" issue
  • It would still enable people to write modules full of content that is pub(crate) without having to type pub(crate) all the time.

Disadvantages:

  • Modules that are half pub(in path_a) and half pub(in path_b) or similar situations won't profit very much, but I think they are rather sparse.
  • Its still less convenient than the current system (you need to type #![pub = pub(...)] in every module; thats better than typing it for every file but still an effort), but I guess that combined with the `inline mod proposal, it would end up being used less, and also I guess the opinion of the lang team is that convenience in this aspect doesn't matter

What do you think?

Screenshots

"collapse directories, but display everything a directory contains if its uncollapsed" mode:

“only show opened files” mode:

8 Likes