Is the way I organize code bad?

I have a standard I've been using for organizing my personal projects. It's something I cobbled together from some standard advice and some tricks I worked out. I've found it very useful, but I'm starting to contribute to an open source project as the main Rust person, and I want to run this by more experienced people before I inflict it on the world.

I wrote out an example project at GitHub - edwardpeters/organizing_rust: A system for organizing modules that I've had success with to show it off in use.

The core idea is to organize imports without a lengthy and frequently changing use block at the head of each module. I do this thru the use of a number of modules that are just containers for re-exporting. So, at the root of the crate are two modules:

  • pub(crate) mod crate_universal, which re-exports stuff that (for the most part) every individual .rs file should see
  • pub(crate) mod interface, which re-exports stuff that other crates should see on a flattened top level.

Then each mid-level module (nested however deep) has

  • pub(crate) mod crate_universal, which re-exports stuff from this module that you would want any other module in the crate to see (this is used by the top-level crate_universal
  • pub mod interface, which similarly provides stuff from this module that should be on the interface for the entire crate
  • mod mod_universal, which re-exports things that should be seen by all the leaf modules of this crate

And finally each leaf module has use super::mod_universal::*. (If a leaf has a lot, it can have its own re-export crates to describe what its parent should include in each of the three categories.)

The idea is that each mid-level module reports up the hierarchy the things that should be on the interface or seen by all implementation files; each mid-level module also passes down the hierarchy anything from that or other modules that should be used across that module.

For example, in this example I want std::fmt::Display and common_parts::Paint to be seen across the entire crate; by re-exporting these from crate_universal,

The goal of this is to offer a combination of flexibility and lightweight standardization. I find that almost every module I write only needs the mod_universal exports from its parent module. Each mid-level module has a lot of flexibility in what it wants its children modules to see, or which of its contents are important enough to other crates to be re-exported into their standard namespace.

Finally, I included a few tests to show how I'm organizing those (I didn't do anything as weird there, I think I used standard practice?), and a couple of macros I use to set up the "standard" prefix at the head of each module.

1 Like

This is a worthy goal, but you can accomplish it without using any glob imports, by using (short) paths. For example, presuming you want to stick to your structure of defining a common namespace used by all modules, you could write:

mod foo {
    pub struct Foo;
}
mod bar {
    pub struct Bar;
}
mod common {
    pub use crate::foo::Foo;
    pub use crate::bar::Bar;
}

mod user {
    // let this be the only import in most modules…
    use crate::common as c;

    struct UsesOthers {
        foo: c::Foo,
        bar: c::Bar,
    }
}

You can even eliminate the mod common part by using the crate root as the common namespace:

// foo and bar defined same as above
use crate::foo::Foo;
use crate::bar::Bar;

mod user {
    use crate as c;

    struct UsesOthers {
        foo: c::Foo,
        bar: c::Bar,
    }
}

(I've declared the one-letter name c on the presumption that a longer one would be more bothersome, and it'll be clear enough if it's used everywhere.)

While having a single module for everything of interest is unusual, using qualified names is very much within the bounds of standard, idiomatic Rust; even portions of the standard library are intended to potentially be used this way (e.g. io::Error from an import use std::io;).

I personally prefer zero glob imports except for same-file use super::*;, and reexports of the form pub use some_private_module::*;; I feel that every glob import harms readability because the reader cannot tell what names it affects. So I very much don't care for your scheme of not just using glob imports ubiquitously, but hiding them behind a macro. I hope you'll consider the alternative I presented above, which still minimizes needed updates to uses.

In any case, if you do use glob imports (or even non-glob imports), please don't obscure them behind macros. Just like standard formatting makes code more approachable, so does using existing language features without giving them new names.

4 Likes

I definitely see your point about macros - really, a lot of why I did that was because I'm still tweaking this whole thing, and so long as that's the case it's nice to be able to do so by changing the macro rather than every file that uses it.

I like the idea of having a reduced path rather than the glob import; that seems an orthogonal choice to what I'm doing with the reorganization modules in the mid-level modules. (i.e., if I wanted to use my mod_universal in place of your common, but do use super::mod_universal as c instead of use super::mod_universal::*, I'd still need to maintain mod_universal to pass imports up and down (and I imagine they'd still use glob re-exports). Does that aspect seem reasonable?

To my mind, mod_universal is an unnecessary complication, and maintaining it is itself import-adjustment-busywork of the sort you object to, just in disguise as “a cleaner structure”. It requires you to answer the question “Do all the children of this module need to use this item?” up front instead of in response to usage, and to modify files that aren't involved in the change (the parent module of the new use site). I figure it makes sense to either

  • build one/few namespaces (modules) of particularly interesting items in your crate (as I suggested above), or
  • let each module decide on its own what to import (“unstructured” regular Rust style)

but I don't see benefits to mid-level aggregation — it's too much time spent thinking about “should”s rather than just writing what imports turn out to be necessary, and no more code than that. It's a distraction from getting actual work done.

Again, this is all my own opinion; others may disagree, and this is a highly subjective matter since it doesn't affect what the program actually does, just how it's written down.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.