Curly brace support for mod

I really do not understand how it is less confusing having to learn:

  • the specific structure of every crate ( because everyone will make a different choice)
  • the actual correct structure
  • that in contrast with #include in C you should only mod a module once, while the wrong way to mod a module from multiple modules can be valid it is almost always wrong and leads to really confusing errors (i suspect this will be the biggest hurdle)
  • not making cycles with modules by accident
  • that yes you are allowed to name the file differently to the module, you just shouldn't
  • how to convince your boss if this fact
2 Likes

From my point of view, you are trying to use two different words for one single concept, based on something that doesn't matter and isn't needed to read the code.

I don't use pub to mean "visible outside the module but not exported outside the crate" by default, and I don't use it to mean "visible outside the module but also exported outside the crate" by default either. To me, it just means "visible outside the module" and the module doesn't care whether or not the crate as a whole makes it visible – that isn't information you need to know to be able to use the module correctly, or to edit it.

It doesn't make sense, to me, to have to reason about the rest of the crate to be able to write a module.

When I write a module:

  • I usually haven't decided whether the module will be publicly visible or not outside its crate;
  • I usually haven't decided whether anything within the module will be publicly visible or not outside its crate;
  • I often haven't decided which crate the module will go into;
  • I often haven't even decided whether the code I'm writing will be a crate of its own, or a separate module within a crate.

To me, a module is a unit of code that makes sense on its own, and I write it to make sense on its own. This makes the code much easier to reason about, because you can view it in isolation and don't need to keep the rest of the context of the surrounding crate in your head to understand it. When writing large crates, this sort of abstraction boundary greatly simplifies the task, because you don't need to understand the whole crate to work with a single module. If the module has dependenices, it doesn't really matter whether they come from a crate that the current crate depends on, or from the parent module and its descendants; all that changes is a few use lines at the top. Really, the crate structure of a big Rust project is something that to me is almost irrelevant, especially because it's so easy to change (you just move some files and mod statements around, and all the pub continue to make sense in their new location) – and in turn that makes projects easier to work on because you don't need to design the crate structure in advance of starting work.

I do use pub(crate) on occasion, but to me it's a warning, with a meaning along the lines of "this API is not actually sound, but I checked the callers and know that this particular crate doesn't use it in an unsound way", and I try to use it as little as possible because that meaning doesn't compose well. Seeing that warning all over the code, just because the module isn't publicly visible, would make the code much harder to read, because it no longer provides those hints about whether the module's internal abstractions are inact or violated.

I don't understand why people would even bother using modules, except as an abstraction boundary – if the module is going to be tied that closely to the crate it's placed in, why is it even a module? (Perhaps the fundamental issue here is that Rust doesn't offer namespacing techniques other than modules, so people use modules just to group related code even though it doesn't form a coherent abstraction on its own.)

6 Likes
cfg_select! {
    target_family = "windows" => {
        mod windows;
        use windows as imp;
    }
    target_family = "unix" => {
        mod unix;
        use unix as imp;
    }
    all(
        target_family = "wasm",
        not(any(target_os = "emscripten", target_os = "wasi")),
        feature = "wasm-bindgen"
    ) => {
        mod wasm_js;
        use wasm_js as imp;
    }
}
6 Likes

Exactly. The use of #[path] for target-specific modules can generally be replaced by either cfg conditionals on the modules or #![cfg(...)] conditionals inside the modules.

1 Like

This rings very true for my experience too.

More than that, during the iterative process of deciding which types should be public (to what extent, usually within the crate) I find it very helpful to see "where" a type is not accessible. The item itself is private? or the containing module is private? ... or some parent module a few levels up is private? Each of those is a decision, usually with a module doc comment explaining the decision. It can help me get back up to speed on what decisions I made in the past (about entire hierarchies of items) and identify what part of the thinking needs to change. Eventually, that helps me tease out what the module structure should look like, based on what mod level visibility needs to be at each step.

I think it's a lot more likely that we fix those problems than that we make things uniformly worse for everyone by effectively making everything have an attached #[path] (with or without a different syntax).

I can appreciate that the explicitness might solve the problem of people not knowing what file corresponds to a module when they see a mod. I expect it would instead introduce the problem of not having any consistency with where a module is placed in general. This is a case of convention-over-configuration, where the conventions affect the ecosystem and the ability to work with various crates across the ecosystem more-or-less consistently. We should be making that more consistent, not less.

3 Likes

Ah, true. I wasn't thinking of nightly features. Though it seems like the macro will be stabilized relatively soon!

Your proposition would have made the situation worse in my case, not better. I would have been able to write mod b = "b.rs"; in a.rs, and it would have worked (even if what I really wanted to do was to write this line in main.rs) which would have tighten a wrong mental model of what mod means. This is exactly the opposite what you want when learning.

Then when creating c.rs that also uses code from b.rs, I would have repeated mod b = "b.rs"; in c.rs (since it would really look like mod means #include like in C/C++), which is absolutely not what I wanted to do. I don’t even know if it can compile. If it doesn’t I would have had an even harder time to understand what I did wrong. But even if it compiles, I do expect that I could get absolutely horribly confusing errors if I have any use of static variables in b.rs.

All of this initial confusion would be completely removed by automod.

5 Likes

I don't particularly care for the suggestion I don't think it adds any value. I think it will confuse inline modules mod { ... }

I think this points at the big difference between us here. It sounds like I tend to make smaller crates where the crate is the abstraction boundary, and where modules really are, as you describe, a form of namespacing and a way to group together related code rather than having forming abstraction boundaries.

Thank you for sharing this, it's in interesting different perspective!

1 Like

There used to be recurring questions about mod vs use in the user forum, until I've added a note to the error message:

help: to create the module foo, create file "src/foo.rs" or "src/foo/mod.rs"
note: if there is a mod foo elsewhere in the crate already, import it with use crate::... instead

and I think it largely solved the problem. This message could have been better if it could suggest actual use path, but unfortunately it's quite tricky, because when the mod items are parsed, the crate's module structure doesn't exist yet.

15 Likes