Add a 'use mod' semantic

I was in those lang team discussions, and I vividly remember talking through these proposals. We didn't decide that it was "not the direction ideal for Rust". We decided that it'd be a lot of additional work to specify a full solution for it that properly took into account things like cfg and path, that we weren't sure we had a consensus on wanting it yet, that the 2018 module system was already running late, and that if we tried to include this additional feature we would almost certainly have failed to ship the 2018 module system at all, which was a headline feature of the 2018 edition.

We recognized at the time that it'd be a substantial usability improvement to not need to write mod. But we also recognized the potential compatibility issues with stray files and with cfg and path directives. We would have needed a proposed solution that took all of that into account, and we didn't have one at hand.

I think we should still consider making a change like this. It'd be a substantial usability improvement.

As one example of a potential solution we could propose: We could handle the common cfg and path pattern for using different modules on different platforms by putting #![cfg(...)] inside the platform-specific modules. People could then write mod module { use target1_module::*; use target2_module::*; }, which would avoid having to duplicate the cfg directives as the modules for all but one target would be empty. (Using use target1_module as module; would require duplicate cfgs to avoid multiple conflicting use directives with the same as module.)

I think if we have a solution for the cfg/path problem, we can then treat other cases of "stray files" as something the compiler can mostly catch, one way or another.

4 Likes

I think more justification is required why writing explicitly a few extra lines is deemed "a substantial usability improvement". For any medium-sized project (e.g. 5+ KLoC source) the overhead of explicit declarations doesn't even register in the list of boilerplate required, while small projects don't need separate modules at all. One can split them if it improves code structure, but nothing requires one to do it. With small projects it's just as good to write everything in one file, possibly introducing a couple of inline modules.

Some languages, like Java, force people into a mindset of splitting source into a confetti of files. This experience is neither universal between languages (more of an exception), nor something to encourage.

This would make it significantly more difficult to understand which files are included when. It's already can be hard enough to understand conditional compilation. Having to jump between files makes it worse. Conditional and unconditional modules would also look the same, unless one reads carefully the first lines for ![cfg]. And it isn't even guaranteed to be on the first lines. Inner attributes commonly go after license boilerplate and module documentation.

4 Likes

The problem here is not about the overhead of writing the line, or how short or long that boilerplate is compared to other parts of the project.

The problem here is having to "mount" files into the module tree at all, rather than having them automatically understood.

This is a problem both for new Rust users (who don't yet know or remember that they need to do so), and for experienced Rust users.

Once you're done writing the_module.rs, it feels like you should be done, until the compiler reminds you that you have to repeat yourself and do two separate actions in order to create a module.

By way of precedent, people made many of the same arguments about omitting extern crate xyz; lines. And yet, I think not having to write those lines has been a substantial improvement.

5 Likes

It feels like this mostly because of habits from other languages. In my opinion, separating file and module structure is a good thing. For example, in the following cases your proposed alternative would be significantly more annoying and would result in a less clear code:

The "annoyances" associated with mod are really far on the list of papercuts which bother experienced Rust users (e.g. duplication of trait bounds on each impl block or lack of trait/bound aliases is a MUCH bigger annoyance) and for new Rust users it's just a minor speed bump which can be "solved" by simple heuristics without a deeper understanding of the system.

4 Likes

I agree that cfg_if is a good example of a problem that would still need solving in a better way. And I don't think we should do this until we have a clear explanation of how we would expect that code to be written under the new approach, in such a way that the new approach is either better or at least not worse.

For the second example, I don't think it'd be heavier or more awkward to write if the mod was implicit and path was unneeded.

But in any case, I also think it would be reasonable to have an "opt-out" of this approach for complex cases that feel it'd be easier by the current method. I don't, however, think that's the most common case.

2 Likes

One question with auto-mounting is where you’d put the possible visibility modifier. I guess modules could default to private and you’d do an explicit pub(vis) use submod; in the parent. Or alternatively modules would all be public, like in Java etc, and only the items inside would have visibilities. Or maybe you could still explicitly write a mod declaration if you want to attach attributes or a visibility.

But what about this? Get rid of mod, don’t have glob automounting, but change the semantics of use PATH such that all components of PATH are mounted if they aren’t yet in the mod tree but are reachable via a chain of uses with appropriate visibilities. Basically, combine mod and use.

2 Likes

Do you have an example of this? Specifically with using a path as opposed to just a module name? It seems if I want to expose foo::unix and foo::windows in my crate I still need to do pub use foo and then two new pub use inside foo? But since this is platform specific modules, how do I tell it to skip those?

(NOT A CONTRIBUTION)

I would frame this differently. The lang team had internal consensus that allowing users to omit mod statements was a good idea, but the community feedback was far more divided. We dropped this part of the proposal mainly for that reason: to concede to the lack of consensus in the community, not because it was too technically difficult.

Along these lines, I also still think this is a change that would make Rust a better language, but if the dissensus in the community remains I do not think this is a particularly productive use of time for the Rust project.

8 Likes

Yes, this would still require a chain of pub(VIS) uses (with possible #[path] #[cfg] etc attributes that currently go with mod), so in deeper module hierarchies not much would change from the status quo. Instead of mod, you'd simply write use everywhere.

But in the "newbie" use case where you have a binary with a main.rs that imports foo.rs and bar.rs, you wouldn't need the mod bar; use bar; dance anymore , which would be an ergonomics and teachability win. [Edit: I meant use bar::*; like in the OP.]

I guess it would be weird that you'd still have to remember to do use foo; first in order to be able to use foo::bar; – or maybe even weirder, use foo; before doing use foo::*;. Perhaps use foo::bar; could implicitly mount the direct child module foo (but use foo::bar::baz would still require a suitable pub use baz; in bar) without making the logic too complicated?

I don't agree, it means you get to declare whether a module is public or not, and also the conditions under which it should be included in the compilation. I think it is an excellent feature, albeit there is a learning curve for people coming from C++.

1 Like

I wouldn't even go that far at the time; some of us at the time didn't see obvious solutions to the problems like cfg/path. It is entirely possible that if we'd worked further we might have found solutions to those problems, and reached consensus on those solutions, but at the time we hadn't gotten that far and didn't have the energy to do so along with all the other work on the module system and the fact that the module system was "running late" for the 2018 edition.

I think there would now be value in working on solutions to those problems, and if we find such solutions, we should see if we have a consensus on them.

4 Likes

What use bar;? That's a useless line. I suppose you meant use bar::*;, but that is neither common nor encouraged. If it were up to me, I'd remove glob imports from the language entirely.

Are you writing code in Notepad? Because an IDE can already trivially do all those things for you. In IDEA:

  • If you just write mod foo; in a file, foo will be highlighted in red, you get an error, and a suggested autofix "create module foo.rs". Click, done. My preferred way.
  • If you try to create a separate foo.rs file in the file tree, you'll get a popup asking if you want to automatically attach the module to the supermodule.
  • If you choose not to attach it, or create the file via some other way than the "create new Rust file" popup, you'll get a "detached file" warning as soon as you open the file. In a single click, you can apply the autofix to attach it to the supermodule.

It's a complete nonissue. At this point, it's frankly way more work to create a detached module and get a compilation error.

And if anyone isn't using an IDE in 2024... Just why? Should we also pull half a thousand of Clipipy lints in the compiler because someone is averse to using Clippy? Or ship a built-in version control? There are proper tools which solve the problem quickly and efficiently, without any negatives for the language.

That's entirely different. extern crate was always pure boilerplate, because it almost never adds any information besides what's already contained in Cargo.toml (in fact, it has less information: no versions or features). Purely duplicating the same list of crates twice doesn't add any value. The current solution fully fixes the problem, without needing to think about extern crate ever again, bar some very niche cases like conditionally including alloc in #![no_std] crates.

The current solution of explicitly listing modules cannot be (soft-)deprecated. There are cases, like conditional compilation, textual macro_rules! scoping, module visibility, #[path] and inline modules, which mean that the explicit listings will have to stay in most of codebases. This complicates the language, not simplifies it! Everyone, including new users, will need to learn and understand several module mounting systems, and be ready to deal with whatever edge cases and conflicts that arise from them.

8 Likes

Please don't insult people. Both experienced Rust developers and new users have a variety of reasons for using a variety of tools. In my case, I use vim.

is not and has never been a requirement of writing Rust, and we don't judge proposed Rust features on the basis of what an IDE can or can't do.

vim file.rs (or, more likely, already in vim, :vsp file.rs). Type contents. Save. Compile. Get annoyed at the requirement for additional boilerplate.

Because while rust-analyzer is an ncredible piece of engineering, in combination with LSP it nonetheless crashes/hangs reasonably often, gives the appearance of hanging on large projects, breaks on anything with a marginally non-standard build system, and is far below the standards of reliability (100%) or performance (instant) I expect from my editor.

And mod modulename; almost never adds any information besides what's already contained on the filesystem.

I think you are drastically overestimating how many projects need to use conditional inclusion of modules, or #[path] directives, versus the number of projects that never use mod in any context more complex than mod modulename;.

mod modulename { ... } is not a substantial complication here, and fits fine as part of the filesystem-based model.

As for macro_rules!, ideally those can work via use just like everything else; in many cases they can.

8 Likes

I think that to design a language counting on tools that LSP, IDE already provide, and assuming that all users are using that, is a pretty bad choice.

It should be the opposite: make it easier for people that use a simple editor, like vim or Notepad.

I love that I can just write fn main() {} and start coding.

Not that users shouldn't try to use tools that improve their productivity, but there's no reason for the language developers to create useless friction.

In the future people will say: who is not using copilot in 2025? ...

6 Likes

I often review code and suggest changes from the GitHub mobile application, which has none of this. It's quicker and easier to do things on my phone while travelling than to get my laptop out, fire up the IDE, set up a hotspot, grab the PR locally, and do things.

In this situation, using the GitHub editor, mod statements are an annoyance, and I would not be upset if mod foo; was replaced by automounting of files.

I find glob imports to be far more of a problem in this situation than I'd ever find implicit mod; without mod, I'd just look at file names, and still be fine, but with use foo::*; use bar::*; … quux(), I have to work out which of foo and bar contain quux (where on my laptop, I'd use IDE assist to jump to definition).

5 Likes

Also check out dirmod for auto declaring modules based on common idioms

I'm curious about why all these questions about cfg and path directives arise as they all seem trivially solved. By that I mean, couldn't this be done while not changing any behavior of existing code?. I'm new to this, so I'm guessing I'm missing some key info.

I'm imaging the a feature that works as follows. When evaluating a file and encountering the line

use A::B

rustc checks if mod A has already been encountered. If so, behave exactly as today. If not, expand the line to

mod A
use A::B

and continue processing the file.

In that case, anything that is currently done with mod will still work, and simple cases can just skip the redundant seeming mod line.

I'm sure I'm missing something that makes this harder, please point it out.

One subtly impactful thing is that Rust code isn't processed in source order like that. Just like you can write mod m; use m::T; you can also write use m::T; mod m; with the items in the opposite order. Furthermore, the mod item could come from macro expansion, and doing name resolution on that macro name requires knowing what modules are loaded in an incremental fix point loop.

Thankfully the absolute worst offenders that would be obviously undecidable aren't possible — textual macro scoping with #[macro_use] requires mod statements and #[macro_export] names can't be used from the crate root — but it's still straightforward to create fun problems using glob imports. Consider:

use moda::*;
use modb::*;

imported_macro!();

You cannot know which mod the macro comes from without mounting the module. But expanding the macro could create a mod statement for either of the modules and use #[path] so the default search location isn't the one that gets used, so you can't know whether to create a synthetic mod item for which modules until after you expand the macro (which requires loading the module).

6 Likes

If the module is meant to be cfg'd, a stray reference can bring it in and cause confusion. I'm not sure how confused autocomplete could get/be when it is suggesting unavailable APIs.

3 Likes

There is something seemingly similar to this in current Rust using crates:

crate macro_exporter:

#[macro_export]
macro_rules! make_mod {
    () => {
        mod macro_exporter {}
    };
}

Then in a downstream crate:

use macro_exporter::*;

make_mod!();

This gives an error message:

error[E0659]: `macro_exporter` is ambiguous
note: ambiguous because of a conflict between a macro-expanded name and a less macro-expanded name from outer scope during import or macro resolution
note: `macro_exporter` could refer to a crate passed with `--extern`