> why there can't be sensible defaults to define modules based on the file system structure alone.
There could be, and in fact, I personally advocated for them. But there was significant community pushback; it turns out many people like to "comment out" entire modules when doing big refactorings. There are also some interesting edge cases, for example, you can use a #[path] attribute on a module to change what the path to the file is; without a mod statement, it's not clear where that would go.
Proof that it is still quite confusing, I tried to remove the mod.rs in my project similarly to that example but now I don't understand how to access these modules.
With the mod.rs in place I can do this in my main.rs:
pub mod foo
and then in a different file do:
use crate::foo::bar::*
But without the mod.rs it doesn't work and I don't know what the correct incantation is...
Edit:
Nevermind, I thought this update meant you could remove the mod.rs completely but this is about declaring foo.rs as a file outside of the foo folder.
If you'd like to have two modules, `main.rs` and `foo/bar.rs`, and you want the latter to be `foo::bar`, you can do this in `main.rs` to avoid needing a foo.rs that just does `mod bar;`:
So I'm not sure I'm a fan of this either. So now for a module with sub-modules, part of it is defined in the top-level directory, and part of it is defined in a sub-directory. Now if I want to move a module from one directory to another, I have to move two items in the file system.
> it turns out many people like to "comment out" entire modules when doing big refactorings.
It seems to me it would be much better to have an "exclude.rs" file or something to opt out of a sensible default rather than forcing extra work in the common case just to support the common case. Or else you could still allow a mod.rs if you want to be explicit about your module contents, and just assume it includes everything in the directory if it's missing
> There are also some interesting edge cases, for example, you can use a #[path] attribute on a module to change what the path to the file is; without a mod statement, it's not clear where that would go.
Again, I don't think a design should be optimized to support interesting edge cases. It should make the common case as simple as possible. If edge cases need to be supported, I'm sure a solution can be found
I am not saying you should love the module system, just trying to provide some context.
(Everyone seems to love and/or hate the module system for various, opposing reasons. It is a great mystery to me. Explaining the module system is like, kind of my white whale.)
Yeah I just think it’s a trend with rust where when a new user complains about a beginner-unfriendly feature, often they are met with an explanation of some esoteric benefit.
I just wonder how much of that is as-hoc reasoning, and it doesn’t seem like it’s a particularly good sign.
I thought this way too until I saw a bunch of people praising how easy the module system was because they felt it was very similar to Python's. I don't know Python well so I can't speak to it.
My working theory: each language does modules in a different way. People think "Oh, a module system, I know this" and then run into issues when it works differently than their language. Just a theory though, I have not been able to validate it, or to figure out an explanation that works for all people.
And yes, more generally: as Rust continues to grow, more and more people will hit more and more edge cases, and they cannot always be fixed, thanks to being backwards compatible. That's just how things are as they grow up. More and more users also brings more and more use cases. It's a feedback loop.
Oof I dunno I'd categorize the Python module system as "makes no sense and I can never get it right" - I'm sure it's consistent but it doesn't feel approachable to me.
Honestly, that idea makes a lot of sense to me, and personally, I find the module system pretty normal seeming for a modern language. I'm used to Perl and CPAN, and it's pretty similar to that, except for the option to use dir/mod.rs, which honestly seems like it's kinda nice for keeping a module contained nicely for those that want to do it that way.
Now I'm more interested in what the complaints about it are, and specifically what they're in comparison to. Either they're not used to using so many modules in a method such as this, or they're honestly expecting some better system for their use case that I'm unaware of, or a little of both, so my curiosity is piqued.
It just really depends. I should have been writing these down over the years. A few common points of confusion off the top of my head:
People expecting to put "mod foo;" at the top of foo.rs, to declare that foo.rs is a module.
People expecting to put "mod foo { }" with the contents of the file inside the {}s to declare that foo.rs is a module.
People expecting that "use" declares a module, not "mod."
People expecting every file inside a directory "foo" to be the contents of the "foo" module, regardless of filename, all concatenated together.
People expecting that mod statements never need to exist because it should be inferred from the filesystem.
General confusion about the privacy rules; that "pub" may not mean that your thing is globally public.
General confusion about crates vs modules; main.rs and lib.rs being in the same directory, but declaring different crates. Not understanding how to import stuff from lib.rs into main.rs, because it feels a bit different than other modules.
People used to also struggle a lot with "use" and paths pre-Rust 2018, but that's been mostly cleaned up at this point.
So a point of comparison would be like a Swift or a Java where module/package definitions are defined implicitly, based on the location in the file system. In contrast, Rust's explicit module declarations seem cumbersome and unnecessary, especially since 90% of the time you're just typing out a structure which is identical to what already exists in the file system. So this could easily be inferred, but it's just not.
I think the same can be said for match statements on enums: in Rust you need to match against the fully qualified type name, when in other languages (including Zig) you can infer everything up to the enum case, since it's already specified by the type you are matching against.
These kinds of things just seem weirdly inconsistent, since in many cases Rust favors inference and elision to remove boilerplate, while in other cases it requires explicitness when there is no technical reason for it to be required, and other languages handle inference just fine.
I actually the module system is one of the few exceptions where it's actually poorly designed. It could very easily have a 1:1 mapping to the filesystem with no need to declare modules to use them. Lot's of people apparently don't like that idea, but most other module systems do it that way, and IMO it would be a lot more intuitive.
> It could very easily have a 1:1 mapping to the filesystem with no need to declare modules to use them.
I would love to see this. The issue isn't that people hate the idea. The issue is not breaking existing projects and workflows when making such a change. If we could find a way to make this work without breaking existing projects and workflows, I think there's support for doing so.
What if we could provide this with tooling? Like, everyone that uses Go for an extended period eventually complains about how it's an error to import a module and not use it... until they enable goimports-on-save and then the problem vanishes (mostly). (My point is not to compare go/rust module systems but to illuminate how a pain point introduced by the language's guarantees can be eliminated by tooling.)
If there is such an easy to define 1:1 mapping, then could there be a rustmods-on-save tool that automatically adds mod statements according to a fixed scheme whenever any *.rs file is created in the current directory or a child directory?
At the very least, we could have a warning if you have any .rs file that isn't being brought in by a `mod` statement, which would help people understand why their code isn't working.
That'd also get us halfway towards just making it automatically work without `mod` statements.
The new module system is already incompatible with pre-2018 edition projects, so I don't know why this argument explains why the current module system doesn't bite the bullet and use the file system hierarchy.
> The new module system is already incompatible with pre-2018 edition projects
Code written for the 2015 edition should generally just work with the 2018 edition module system. That's part of what we worked to ensure, and that's why we didn't mix in changes that might have reduced that compatibility.
Rust has an RFC process, which is great in many ways. But for something like a module system where everyone has an opinion, it can result in an over-engineered solution that tries to satisfy everyone.
I think the main reason why the foo.rs foo/bar.rs stuff was done was so that the tabs in an editor have informative headings. Instead of ten times mod.rs you have foo.rs, baz.rs, test.rs and so on. Personally I don't like it either (for the same reason as you) but the old way is still possible, even on the 2018 edition, so that's what I use.
Nice to hear that there has been some progress here. I recall trying Rust 4+ years ago and my experience with the module system really left a lot to be desired from someone most experienced with Python's module system.
I was confused by rusts module system too, after having used es6 modules in js and python but then I realised the module systems couldn't be compared.
In dynamic langauges like python or js u can individually use modules but in rust everything has to link back to either main or lib if I'm not mistaken.
This was what enabled me to get rid of preconceived notions of how a module system should work.
> why there can't be sensible defaults to define modules based on the file system structure alone.
There could be, and in fact, I personally advocated for them. But there was significant community pushback; it turns out many people like to "comment out" entire modules when doing big refactorings. There are also some interesting edge cases, for example, you can use a #[path] attribute on a module to change what the path to the file is; without a mod statement, it's not clear where that would go.