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.