I like a lot in your proposal
and much prefer it to @aturon’s.
In general, I would rather have a simple system even if it requires some boilerplate, and does not allow fine-grained encapsulation.
I’m going to build on it, by considering those related concepts in turn:
-
code organization, also known as “How do I access this item?”
-
privacy, also known as “Who has access to this item?”
Note: I will not address the extern crate issue; I quite like the current situation and I am not sure that it needs changing.
I strongly believe that code organization should, by default, be tied to the filesystem much like Python modules are:
- Python is a widely used language, and therefore a lot of people are going to be familiar with it,
- This de-facto eliminates a lot of boiler-plate,
- Commenting out is easily carried out by renaming the file (changing the extension, or using a leading underscore like unused items today).
So, for code organization, I would simply:
- make the module hierarchy reflect the directory/file hierarchy,
- only include a directory if it does not start with an underscore and contains a
mod.rs file (or lib.rs, bin.rs),
- only include a file if it does not start with an underscore and ends in
.rs,
- error out if any “to be included” file has a name which is not a valid identifier in Rust.
This makes code organization intuitive.
This does not immediately address the Facade Pattern, and I do not think it is necessary to address it here.
Actually, I would even favor deprecating the mod keyword altogether.
The only other use of mod I have in my tests is #[cfg(test)] mod test and I feel like unit testing would benefit from being more keenly integrated in the language. The fact that I like small files and large test suites also means I favor putting tests in a separate file, and therefore I could easily envision:
foo/
test/
bar.rs
baz.rs
mod.rs
bar.rs
baz.rs
lib.rs
Then:
-
foo/test/bar.rs is considered a child module of bar.rs, and therefore gets full access to its private items (access still requires super: use self::super::*; at the top is easy enough),
- If really necessary
#![cfg(test)] would decorate foo/test/bar.rs, though to be honest I’d be keen on inferring it (aka, if the test directory does not contain a mod.rs, then the files inside are test files for its .rs siblings) and have the compiler report test/xxx.rs if there is no xxx.rs file to test.
Note: I much prefer this organization to Java’s, because it keeps the tests close to the code, instead of having to navigate to another subtree entirely; at the same time, it’s tidy: all tests are neatly tucked into a separate directory. Oh, and having two files means visualizing both in parallel works even in editors that do not allow opening the same file twice without contortions…
I am of the opinion that privacy should be the default, and public should be opt-in, for the simple reason that it forces a conscious choice of making something public, thus avoiding accidental leakage of internal details. It also does not hurt that the compiler messages can immediately spot the issue without ambiguity when trying to access a (too) private item so it’s an easy compilation error to solve.
Regarding the different levels of privacy, I am afraid that too much is too much: the vast array of choice (pub, pub(crate), pub(restricted), pub(mod), pub(self), …) is simply bewildering.
Encapsulation is certainly a desirable property, however too many options may just be that, too many. At some point there are diminishing returns eating into the language’s complexity budget. As such, I’d favor coarser granularity because it’s simpler.
I would simply use 3 levels of privacy:
- the default is private, restricted to the current module and its children,
-
pub means that the item is public, immediately; that is, if foo/bar/baz.rs contains a pub struct Hello; item, then $crate::foo::bar::baz::Hello is accessible outside the crate (and each step of the path is accessible individually),
-
pub(crate) is similar to pub though restricting the scope to the crate itself.
This does mean that there is no strong encapsulation within a crate; however at the same time such encapsulation would be an edit away from not existing, so I am not sure how valuable it is. I could, maybe, be convinced that pub(super) would be a useful 4th level. Maybe.
Note: I’d really have a dedicated keyword rather than pub(crate) which feels like a second-class citizen; maybe local or protected?
With that in mind, the facade example would require in future/mod.rs:
pub use self::{and_then::AndThen, flatten::Flatten, flatten_stream::FlattenStream, ...};
As well as each individual item being declared either pub or pub(crate) to be visible from future.rs (cannot re-export what you cannot access).
It’s a tiny bit of boilerplate, for sure, but:
- it’s literally a couple keystrokes,
- it can be made to work with glob patterns easily enough to get “inline” module (
pub use sub_module::*;),
- it provides nice navigation benefits: if you have
future::AndThen you go to future/mod.rs where you have a nice redirection panel pointing to either future/and_then.rs or future/and_then/mod.rs.
Note: I would favor relative paths over absolute paths, but this seems completely orthogonal.
Platform specific implementations require two annotations, if one wishes to paper over the differences:
- in the module itself
#![cfg(unix)],
- in the containing module:
#[cfg(unix)] pub use self::unix::*;
However I see the two annotations as playing distinct roles:
- the latter, decorating
pub use, decides whether to re-export the symbols or not,
- the former, decorating
unix.rs, allows using unix-only functions inside the module.
This does not seem completely unreasonable to me. Is it so widely common that it requires more attention?