Simpler modules to contain procedures

It's a good practice to split functions into smaller. For example it's better to have init_logger procedure called from main rather than having the same code directly in main function.

The problem with smaller functions is that they contaminate parent scope. So it's hard to get the order in which functions are executed right.

But particularly they're obtrusive in outline window in IDE.


That's why I've came up with concept of simplified modules to contain such small functions.

The syntax is super mod name { .. }.

Such modules are always private to containing them modules and all inner items are pub (super). Usually names of such modules must be the same as of parent function. And in addition they inherit all imports from parent modules as if use super::* has been placed at the top.

Example:

use std::sync::{Arc, atomic::AtomicBool};

fn main() {
    main::init_logger()
    let terminate = Arc::new(AtomicBool::default())
    main::register_signal_handling(&terminate)
    ...
    continuation()
}

super mod main {
    fn init_logger() {
        ...
    }

    fn register_signal_handling(terminate: &Arc<AtomicBool>) {
        ...
    }
}

fn continuation() {
    ...
}

Perhaps it should be possible to put them in separate files:

super mod module_name;
// File with module_name should exist in the same directory

Nesting is supported as well.


To sum it up, in comparison with regular modules there's no use super::* at the top as well as no visibility modifiers, so code is easier to type and more transparent to read. Another plus is that functions calls in parent function have their own "namespace" so they're easier to discern and helper procedures looks differently from more important functions.


What community thinks about this design and about usability of the feature?

The tiny amount of reduced boilerplate doesn't warrant a new feature with novel syntax.

5 Likes

The purpose here is rather to simplify refactoring, promote good code practices, and be more precise in intent.

You can achieve the same with a simple proc macro. But a new syntax has a higher bar to clear. Unless there is something that a macro can't do, or what would be unreasonably difficult to do, there is no need for syntax additions.

1 Like

It looks like you're reinventing function-local functions:

fn main() {
    init_logger();
    register_signal_handling();
    continuation();

    fn init_logger() {}
    fn register_signal_handling() {}
}

fn continuation() {}
8 Likes

You save a couple use statements, but then every person learning Rust has to learn what super mod is and how it works.

This seems like a very narrow case (I presume it only works in the parent module, not even crate-wide). For helper functions, I'd prefer functionality around defining a custom "prelude" for crates.

2 Likes

I'll admit that I was mildly surprised when I found out items with unqualified visibility within a module were visible to child(?) modules.

Due to this I would probably then suggest pub(self) <item> or bringing back priv <item> as the syntax for this.

1 Like

pub(self), priv and default visibility are all equivalent. As visibility works in Rust, you are always visible to the subtree of some module, or to the whole world.

Do you have a straw man syntax for not being visible to submodules, then? I'm not clear on why it should be impossible to represent in theory, and while syntax is a consideration, I don't think it should be a primary blocker if the idea makes sense.

The syntax is indeed the least of the worries. The biggest problem is that your proposal would be a radical departure from the current Rust's visibility system. I don't know how practical something like that would be to implement.

It would also be weird if an item could declare which of the submodules wouldn't be able to access it. Imho the only reasonable way for that feature to work would be "this item is visible in the current module, but not any of its submodules of supermodules". However, traits, type impls and enums also have their own namespaces. There could be some undesirable interaction between namespaces and visibility based on modules, and those based on impls and enums.

Overall, personally I think that feature would be more trouble than it's worth. You can currently get similar results in a few different ways. You can declare items inside of functions, making those items inaccessible to any other functions. You can also create a submodule with a slim public API (possibly consisting of a single function or type), and move all items which you don't want exposed into its private submodules.

That's the most common way to organize code in Rust. E.g. look at the stdlib: you have toplevel modules, like collections or mem, with a relatively small public API, and inside them you have individual private modules, which implement the respective API items. Anything which is private within those submodules can't be seen by other submodules, separating the private implementation details.

I generally agree, here's how I would see this working if it were a thing, though:

  • Indeed, I mean only that it would be "current module only, no submodules" - the intent is that such "very private" items can be changed without worrying about submodules using them, the same as any other visibility restriction.
  • I would only expect "very private" to only hide for other mod namespaces, e.g. trait and type impls can access them within the strictly same module, but I can see the argument that there could be weird interactions.
  • Nesting items within an fn is nice, except for two issues:
    • You need to define such items before the implementation. It's pretty easy for this to mean you have to scroll past pages of items just to see that the "actual" body is just imp(args) (or even worse, that it's not). It would be nice if items could be placed after their uses, like in modules, but that's a different request, and I don't feel too strongly.
    • And of course, you might be extracting common functionality between multiple functions, but not make it visible to sub-modules since it's an implementation detail.
  • mod impl { pub <item>... } is in fact the best option (and in fact, I am literally writing such code right now!), but it is a bit weird especially for the fn main() case, to make the actual implementation wrapped up, for the effective purpose of hiding other members of the module. It has tenure, e.g. the sealed trait pattern, but it does feel like a kludge that is ugler than Rust generally is for me.

Overall, not a big deal, but it does (mildly) confuse me that the visibility system is missing it.

Yes, that's valid especially when function contains a piece of documentation at the top and there's no chance to spot super mod name { ... }.

Perhaps something like this would be better:

use std::sync::{Arc, atomic::AtomicBool};

fn main() {
    main::init_logger()
    let terminate = Arc::new(AtomicBool::default())
    main::register_signal_handling(&terminate)
    ...
    continuation()
}

mod main {
    use fn init_logger() {
        ...
    }

    use fn register_signal_handling(terminate: &Arc<AtomicBool>) {
        ...
    }
}

fn continuation() {
    ...
}

Actually, no you don't. It's more common to, because if you want a trailing return expression it needs to be after any items, but it's perfectly valid for items to come after statements which use them.

2 Likes

That is good to know!

So, I've ended up with the following pattern:

use std::sync::{Arc, atomic::AtomicBool};

fn main() {
    main::init_logger()
    let terminate = Arc::new(AtomicBool::default())
    main::register_signal_handling(&terminate)
    ...
    continuation()
}

mod main {
    pub fn init_logger() {
        ...
    }

    pub fn register_signal_handling(terminate: &super::Arc<super::AtomicBool>) {
        ...
    }
}

fn continuation() {
    ...
}

From what I see, the OP is advocating for smaller modules, not simpler modules. This change inherently increases the complexity of the language, and doesn't really gain any simplicity in the process.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.