[Pre-RFC] Partial Impl Blocks
Table of Contents
-
Summary
-
Motivation
-
Guide Level Explanation
-
What Are Partial Impl Blocks?
-
Why This Helps
-
-
Reference Level Explanation
-
Aggregation Semantics
-
Generic and Where-Clause Matching
-
Conflict Detection
-
-
Benefits And Drawbacks
-
Interaction With Existing Language Features
-
Implementation Strategy
-
Overwritable Annotation
-
Open Questions
-
Explicit marking of Partial Impl Blocks
-
Scope of Partial Impl Blocks
-
Summary
#summary
This proposal introduces partial impl blocks, a language feature that allows splitting the implementation of a single (Trait, Type) pair across multiple impl blocks with identical headers.
The compiler conceptually aggregates these into a single logical impl before coherence and type checking.
The goal is organizational ergonomics: improving maintainability and readability of large trait implementations without altering Rust’s safety, coherence, or dispatch semantics..
Motivation
#motivation
Large Rust codebases often encounter monolithic impl blocks when traits with many methods are implemented. This is especially common in scenarios like:
-
Traits with many associated items
-
Macro-generated methods mixed with handwritten code
-
Feature-gated subsets of methods (
#[cfg]) -
Domain partitions within a trait (e.g., I/O vs diagnostics)
Current workarounds (splitting traits, macros, pervasive #[cfg]) lead to:
-
Reduced readability
-
Higher cognitive load
-
Harder refactoring
Partial impl blocks aim to address these costs by enabling decomposition into focused blocks that are conceptually aggregated before semantic analysis.
Guide-level Explanation
#Guide_Level_Explanation
What Are Partial Impl Blocks?
A partial impl block is an impl Trait for Type whose impl header (trait, type, generics, where-clauses) exactly matches other partial impls in the same crate. All such blocks are treated as contributing to a single logical implementation.
Example:
impl ExampleMethods for Example {
fn method_a(&self) { /* … */ }
fn method_b(&self) { /* … */ }
}
impl ExampleMethods for Example {
fn method_c(&self) { /* … */ }
fn method_d(&self) { /* … */ }
fn method_e(&self) { /* … */ }
}
Conceptually equivalent to a single impl with all methods combined:
impl ExampleMethods for Example {
fn method_a(&self) { /* … */ }
fn method_b(&self) { /* … */ }
fn method_c(&self) { /* … */ }
fn method_d(&self) { /* … */ }
fn method_e(&self) { /* … */ }
}
Why This Helps
Separation of Concerns
Partial impl blocks allow logical grouping of responsibilities (e.g., macros vs handwritten code, feature-gated sections vs core behavior).
For example, something like this would be possible:
root/
src/
traits/
mod.rs
// Trait defined here for example:
module/
mod.rs
// struct defined here for the example
core_impl.rs
// Core implementation
windows_impl.rs
// Windows Specific Code
linux_impl.rs
// Linux Specific Code
feature_1_impl.rs
// Feature 1 Specific Code
feature_2_impl.rs
// Feature 2 Specific Code
Macro Ergonomics
Macros can generate only part of the trait implementation, allowing partial code generation alongside human-written code cleanly.
That would be an example:
pub trait Example {
fn required(&self) -> String;
fn generated_default(&self) -> String;
fn optional_override(&self) -> String;
}
macro_rules! impl_example_defaults {
($type:ty) => {
impl Example for $type {
fn generated_default(&self) -> String {
format!("default generated for {}", stringify!($type))
}
// The macro does not implement `required`
// and `optional_override`,
// leaving for the dev to implement them manually:
// fn required(&self) -> String { ... }
// fn optional_override(&self) -> String { ... }
}
};
}
pub struct MyStruct {
pub name: String,
}
impl_example_defaults!(MyStruct);
// Now, for this block, the human doesn't need to implement the generated_default() method.
impl Example for MyStruct {
fn required(&self) -> String {
format!("required logic for {}", self.name)
}
fn optional_override(&self) -> String {
format!("custom override for {}", self.name)
}
}
Cleaner Feature-gating
Feature-specific methods can live in separate partial blocks instead of being interspersed with #[cfg] conditionals.
impl ExampleMethods for Example {
fn common_method(&self) { /* … */ }
}
#[cfg(feature = "foo")]
impl ExampleMethods for Example {
fn foo_specific(&self) { /* … */ }
}
#[cfg(feature = "bar")]
impl ExampleMethods for Example {
fn bar_specific(&self) { /* … */ }
}
Tooling and UX
Tools like rust-analyzer and rustdoc could show these aggregated blocks as a single logical impl for readability and navigation.
Reference-level Explanation
#Reference_Level_Explanation
Aggregation Semantics
Partial impl blocks:
-
Must have identical trait, type, generics, and where-clauses
-
Must exist in the same crate
-
Are collected after macro expansion and name resolution, but before coherence and type checking
-
Are aggregated into a single logical impl for downstream passes.
Generic and Where-Clause Matching
Only blocks with semantically equivalent headers aggregate.
Conflict Detection
If two blocks define the same method unconditionally in the same build configuration, the compiler should emit an error. Conditionally exclusive definitions (#[cfg]) are allowed.
Benefits and Drawbacks
#Benefits_And_Drawbacks
Benefits
-
Enables structural decomposition of large impls
-
Improves macro and feature gating workflows
-
Reduces reliance on artificial trait splitting
Drawbacks
-
Requires a new compiler aggregation pass
-
Tooling must support navigation and diagnostics
-
Potential for scattered impls without style conventions
Tooling updates (e.g., rustfmt, rust-analyzer) and proposed lints (like too_many_partial_impls) could mitigate these concerns.
Interaction with Existing Language Features
#Interaction_With_Existing_Language_Features
-
Macros: Macro-emitted impls naturally participate.
-
Conditional Compilation: Works unchanged.
-
Coherence: Orphan and overlap rules remain unchanged; only the timing of aggregation shifts.
Implementation Strategy
#Implementation_Strategy
-
Aggregate after macro expansion and HIR lowering
-
Group by equivalent headers
-
Proceed with coherence and type checking
This keeps the existing compilation model intact.
Annotation: #[overwritable]
#Overwritable_Annotation
A suggestion raised in the thread proposes a special annotation like #[overwritable] to control macro-generated methods:
-
A macro could generate default method implementations marked
#[overwritable]. -
If a human-written
partial impl blockprovides an implementation for the same methods defined in the macro, the compiler would prefer that instead of the macro version. That block should not be annotated with#[overwritable] -
Without
#[overwritable], macro definitions would not be overridden.
This would be an example:
```rust
pub trait Example {
fn required(&self) -> String;
fn generated_default(&self) -> String;
fn optional_override(&self) -> String;
}
macro_rules! impl_example_defaults {
($type:ty) => {
impl Example for $type {
#[overwritable]
fn optional_override(&self) -> String {
format!("default generated for {}", stringify!($type))
}
// The macro does not implement `required`
// and `optional_override`,
// leaving for the dev to implement them manually:
// fn required(&self) -> String { ... }
// fn optional_override(&self) -> String { ... }
}
};
}
pub struct MyStruct {
pub name: String,
}
impl_example_defaults!(MyStruct);
// Since in this example, the human provided another version of the optional_override method, the compiler would prefer the human version over the macro version.
// Only methods annotaded with #[overwritable] follow this behavior;
impl Example for MyStruct {
fn optional_override(&self) -> String {
todo!()
}
}
This idea offers a controlled mechanism for default vs. override semantics in partial impl blocks, similar in spirit to default methods in specialization RFCs (but scoped to partial impl definitions). Elaboration and consensus on this annotation are open topics for discussion.
Open Questions
#Open_Questions
1. Explicit Marking of Partial Impl Blocks
Three alternatives are under consideration:
-
A contextual keyword:
partial impl Trait for Type { … } -
An attribute:
#[partial] impl Trait for Type { … } -
No explicit marking, relying on tooling
Explicit markers improve clarity and diagnostics, while implicit aggregation keeps syntax minimal.
2. Scope of Partial Impl Blocks
The extent to which partial impl blocks may be placed in a crate is debated.
crate-scoped
partial impl blocks may live anywhere in the crate for maximum flexibility.
module-scoped
Partial impl blocks for a given type must be inside the module where that type is defined (including submodules). For example:
src/
macros/
// Macro definitions
mod.rs
module_for_trait_defs/
// trait ExampleMethods is defined here
mod.rs
module_for_struct_def/
mod.rs
// Struct Example is defined here
// Partial impls for Example (for any trait)
// must be here or in submodules
If a macro defines a partial impl block for Example, it could only be invoked inside module_for_struct_def and it's sub-modules. However, the macro code itself could be located elsewhere.
This would be a little more complex to implement, and might win a topic in the drawbacks or the implementation strategy section if it's choosen.