Pre-RFC: module-level generics

Abstract

This proposes allowing modules to be generic in order to reduce boilerplate:

mod self<Executor: Spawn>;
struct Foo {
executor: Executor
}
}

impl Foo {
// ...
}

would be equivalent to:

struct Foo<Executor: Spawn> {
executor: Executor
}
}

impl<Executor: Spawn> Foo<Executor> {
// ...
}

Motivation

There are cases when a crate author wants to support several strategies for the crate, but adding the appropriate generic that is repeated everywhere decreases readability. Examples may include:

  • Geometry library wanting to abstract between f32 and f64
  • Async runtime - people usually want one per program
  • Using Rc vs Arc for whole library.
  • Abstracting over string representation (String vs &str vs Cow<'a, str>)
  • Combination of those above - a library may want to abstract over various orthogonal things!

There are currently two ways of doing it, each with its negatives:

  • Generics over each type manually - lot of boilerplate, less readable
  • Features - this is actually an anti-pattern as features must be additive, but people use is anyway

Proposal

Allow generics (with trait bounds) for the whole module, probably implemented as syntax sugar, inserting the generic into every struct, enum, type, trait and impl block. There are two ways of writing it:

  • mod self<T>; - for file modules
  • mod foo<T> { /*... */ } - for submodules within a file

Using the modules:

  • use geometry::<f32>::Point; - brings parametrized point in scope
  • use awesome_http::<tokio::Executor>; - brings awesome_http to scope. All accesses through awesome_http will have implicit tokio::Executor parameter.
  • let user = my_lib::<String>::User::load("/var/lib/stuff/user")?;

Rules

  • It's an error to import the module without the generic argument specified.
  • It's an error to shadow type names.
  • All module-level generic parameters can be thought of as being inserted at the beginning of generics of every item defined in the module
  • Child modules inherit the generics via super

Whaaat? This looks too strange, I don't like it!

Yes, I realize it looks strange and I don't know of any other language having this feature. Think of it as something similar to self parameter. You may write dozen of functions, each having parameters name, age, address, but what people usually do is to group them under a common struct that's used everywhere, where needed. In the generic world, the other extreme is avoiding implementing function parameters. :wink:

7 Likes

See also:

8 Likes

Oh, was trying to find if someone had the same idea and couldn't find it. Thanks a lot!

If we can have entire crates parameterizable by generics, we might be able to precompile crates for certain types.

I think it'd be good to support parametrizing of the whole crate too, that was my previous idea. I quickly realized that modules need to be parametrized too, because there is a quite common case when the types are the same: Error types.

Precompiling them is an interesting idea, I wonder how one would choose which types should be precompiled. Maybe the author could put hint into Cargo.toml?

1 Like

I really like the idea! I guess that submodules should inherit the generics of their parent modules, so this works:

pub mod foo<S: ToString> {
    pub mod bar {
        pub struct Stringify(S);
    }
}

pub use crate::foo::<String>::bar::Stringify;

So a crate with a generic root module is equivalent to a generic crate.

My question is, should this be allowed?

type Generalized<T: ToString> = crate::foo::<T>::bar::Stringify;

Trait bound on type aliases is entirely ignored, so that would be a problem.

You're right, so the : ToString can be removed, but that shouldn't be a problem.

Yes, child modules should inherit parent parameters. If it was implemented as a simple syntax sugar, it'd effectively become type Generalized<T> = crate::foo::bar::Stringify<T>;, so yes, it should be allowed.

What happens to const and static items in these modules? I don't think it needs to be anything special, but it should be worth mentioning. Rust doesn't currently allow generic const and static items, and for good reason, I think, at least in the case of static. Would they just be non-generic? Would they then be prevented from accessing other implicitly-generic things without explicitly doing something like self::<()>::const_function()?

Edit: my apologies, I wasn't paying attention to the date. No need to answer this if the proposal isn't going anywhere for now.

Good question, maybe prohibit defining consts and statics in generic modules for starters?

1 Like