Pre-RFC: Transparent Modules

(I wrote this up in response to #t-lang > mod _ instead of const _?)

Motivation

Rust modules broadly serve three purposes:

  1. API organization: Modules are used to group related items together. std is a great example of this: std::fs contains filesystem-related functionality, std::fmt formatting stuff, and so on.

  2. File organization: Modules are used to split the internal implementation over multiple files, without affecting the public API. Again using std as an example, vec::Drain is defined like this:

    pub use self::drain::Drain;
    
    mod drain;
    

    It's not uncommon to instead write pub use self::drain::*.

  3. Field access Restrictions: Modules can be used to limit which parts of the code have direct access to a struct's fields.

    This is especially useful if the fields need to maintain some kind of invariant: A small number of carefully-implemented, low-level APIs have direct access to the fields and always maintain the invariants, and higher-level APIs are implemented in terms of the lower-level ones, without direct access to the fields, and thus without needing to worry about the invariants.

    I've personally written code like the following (though don't have an example from some prominent ecosystem crate):

    mod foo_fields_acessible {
        pub struct Foo { ... }
        
        impl Foo {
            // ... low-level API
        }
    }
    pub use self::foo_fields_accessible::*;
    
    impl Foo {
        // ... high-level API
    }
    

The pub use approach works reasonably well, but I've personally run into a number of annoyances with it:

  • It feels like unnecessary overhead.
  • rust-analyzer gets confused about where to add new imports.
  • If I do pub use self::child::* for all my child modules for consistency, but one of the children has at most pub(crate) items, then unused_imports complains.
  • Declarative macros can't easily generate *_foo_accessible modules.

Most of the individual annoyance can probably be fixed by improving the indivdual features, but purposes (2) and (3) seem common enough that supporting them with a dedicated syntax seems reasonable to me.

Explanation

(The "transparent" terminology is open for discussion).

TODO: Verify that transparent mod can be implemented as a weak keyword

Transparent modules are modules that do not affect the public API.

pub mod vec {
    pub transparent mod drain {
        pub struct Drain { ... }
    }
}

// This is fine
fn test() -> self::vec::Drain { ... }

// This is an error
fn test() -> self::vec::drain::Drain { ... }

Transparent modules may be unnamed if they are inline or have a #[path = "..."] attribute:

pub transparent mod {
    pub struct Foo { ... }

    impl Foo {
        // ... low-level API
    }
}

impl Foo {
    // ... high-level API
}

$vis transparent mod foo; is essentially equivalent to mod foo; $vis use self::foo::*;

The following behaviors of modules are unaffected by this RFC:

  • The parent module's items are not automatically in scope, not even for transparent inline modules.
  • The parent module can be accessed via super::.

It is an error for multiple transparent modules, or the parent module, to define items with conflicting names.

Transparent modules include a visibility qualifier for consistency with non-transparent modules, and to allow transparent modules to use pub on its own items to indicate its own public API, while still letting the parent control how far the those items should actually be exposed.

The compiler should:

  • Not print any warnings if all items in a transparent module are less visible than the module's visibility.
    • If it does print a warning, it should be a new, dedicated lint.
  • Use the following rules when needing to printing the full path to an item defined in a transparent module:
    • If the item is public, i.e. accessible outside the transparent module, do not include the name of the transparent module in the path.
    • If the item is private, i.e. not accessible outside the transparent module:
      • If the transparent module is named, use the module's name, optionally with some indication that the module is transparent (e.g. std::vec::{drain}::PrivateSomething).
      • If the transparent module is unnamed, synthesize an identifier, similar to what is done for closures (e.g. {transparent@<location>}).
2 Likes

To what extent will the implementation of restrictions obviate/diminish the need for this?

I'm assuming you primarily refer to mut restrictions on struct fields. I don't think that would diminish the need for this at all.

If you want to restrict mutable access to struct fields to a subset of code, that subset must still be located in its own module, with the struct itself accessible outside of that module.

pub transparent mod {
    pub struct Foo {
        pub mut(self) bar: ...
    }

    // Limited direct mutable access to bar here.
}

// Only immutable access here (or mutable access via accessors defined in the module above)

unsafe fields may help with safety-related restrictions, but even then it might be desirable for all direct field access to be isolated.

Restrictions also don't help at all when you want to use modules (files) for code organization without affecting the public API.

3 Likes