Idea: trait modules (bad name, please bikeshed)

The name is bad - these are not really traits nor are they modules - but these concepts are roughly close enough to capture the idea, so let's use them for the discussion until someone can come up with something better.

The problem

Consider, for example, default field values in Serde:

#[derive(Deserialize)]
struct Foo {
    #[serde(default = "default_bar")]
    bar: i32,

    #[serde(default = "default_baz")]
    baz: i32,

    #[serde(default = "default_qux")]
    qux: i32,
}

fn default_bar() -> i32 {
    1
}

fn default_baz() -> i32 {
    2
}

fn default_qux() -> i32 {
    3
}

This can be quite cumbersome - both to write and to read. The alternative is to move all these functions into the attributes - but that will just make it ugly and messy and you'll lose rust-analyzer support. Especially if they are left as strings.

So what am I proposing?

My idea is inspired by C#'s partial classes, which allow splitting class definition between an auto-generated file and a human-maintained file.

(in Rust's case, we don't need to split to different files (since the macro generates code in the same file) and it's not really a "split" because in Rust you can already have multiple impl blocks for each type)

A trait module can be declared - either as a standalone or in the context of a type, and it would be nice to also have generics support:

trait mod MyType::my_trait_module<T> {
    const MY_CONSTANT: i32;

    type MyAssociatedType: Send + Sync + Debug;

    fn my_function(value: T) -> self::MyAssociatedType;
}

And then you'd have to implement it (in the same crate. Maybe even in the same file)

impl<T> mod MyType::my_trait_module<T> {
    const MY_CONSTANT: i32 = 42;

    type MyAssociatedType = fn() -> T;

    fn my_function(value: T) -> self::MyAssociatedType {
        // can't really think of how to implement this, I've programmed myself
        // into a corner by adding too many concepts to the example.
    }
}

The content of the trait module can be used as if were a module (though it does not create it's own encapsulation context) - e.g. MyType::my_trait_module::<f32>::my_function(3.14).

Why even do that?

Let's take the example from the problem statement. If this feature is implemented, Serde could add support for it that'd look something like this:

#[derive(Deserialize)]
struct Foo {
    #[serde(default = fn)]
    bar: i32,

    #[serde(default = const)]
    baz: i32,

    #[serde(default = fn)]
    qux: i32,
}

Which will cause the proc-macro to generate:

trait mod Foo::serde_defaults {
    fn bar() -> i32;

    const baz: i32;

    fn qux() -> i32;
}

And then the user, in the same module, would have to write:

impl mod Foo::serde_defaults {
    fn bar() -> i32 {
        1
    }

    const baz: i32 = 2;

    fn qux() -> i32 {
        3
    }
}

So you still need to write it as functions? How is that an improvement?

This will only be an improvement if we can get help from rust-analyzer. Having just the trait mod without the impl mod would be an error, which rust-analyzer would detect and offer a code action to generate the entire impl mod block. If you add new methods to the trait mod without implementing them in the impl mod block it'd also be an error, and rust-analyzer should offer to fill the missing ones as well (just like it does with regular impl Trait blocks)

Other than that, it's also nice to not pollute the global namespace (even if the pollution is contained inside a module) and the namespacing is a bit more organized this way - but these advantages, without the rust-analyzer support, would probably not justify such a feature.

This seems quite close to having an ordinary trait but without Self type (or with some meaningless choice of dummy type, e. g. (), for Self).

This way, there would be a compilation error immediately in case you're missing the impl, but assuming in your described use-case the derived impl is making use of this impl anyway, it you would usually always create a compilation error in practice, after all.

With a regular trait, rust-analyzer does not know to offer to generate the impl Trait block.

If r-a could turn “missing impl for mod” into a “generate mod impl” intention, then turning “missing impl for trait” into a “generate trait impl” intention should be just as possible.

1 Like

When rust-analyzer sees a trait definition, it doesn't know which type is supposed to implement it. Or which types - since multiple types are allowed to implement the same trait. Module traits have one-one relationship between the definition and the implementation.

If you're just asking for Trait::function, yes, but you can also ask for a specific type's impl, such as <Self as Trait>::function or <() as Trait>::function.