[Pre-RFC] Partial Impl Blocks

[Pre-RFC] Partial Impl Blocks

Table of Contents

  1. Summary

  2. Motivation

  3. Guide Level Explanation

    • What Are Partial Impl Blocks?

    • Why This Helps

  4. Reference Level Explanation

    • Aggregation Semantics

    • Generic and Where-Clause Matching

    • Conflict Detection

  5. Benefits And Drawbacks

  6. Interaction With Existing Language Features

  7. Implementation Strategy

  8. Overwritable Annotation

  9. 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 block provides 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:

  1. A contextual keyword:

    partial impl Trait for Type { … }
    
  2. An attribute:

    #[partial]
    impl Trait for Type { … }
    
  3. 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.

3 Likes

Can you give an example of traits that have enough methods to motivate this?

Also, the mention of cfg seems odd to me. You can attach cfg to a method, and once it's stable, you can use cfg_select! over a group of methods to only write the cfg once:

trait BigTrait {
    fn one(self);
    cfg_select! {
        unix => {
            fn two(self);
            fn three(self);
        }
        windows => {
            fn four(self);
        }
    }
}

impl BigTrait for () {
    fn one(self) {}

    cfg_select! {
        unix => {
            fn two(self) {}
            fn three(self) {}
        }
        windows => {
            fn four(self) {}
        }
    }
}

I may be the wrong person to evaluate the desirability of this, though; I tend to always consider it a bug when a type has multiple impl blocks that aren't all in the same module, or multiple impl blocks in the same module for any reason other than having different bounds.

1 Like

I proposed something similar a while ago, though with a very different motivation:

Thank you for the example — it illustrates nicely how cfg_select! (on nightly) can help group compile-time conditionals in one place. It’s great to see that’s something Rust has been experimenting with. However, there are still reasons why that doesn’t fully replace the motivation for the partial impl blocks proposal.


Tools and macros like cfg_select! can reduce #[cfg] boilerplate, but they don’t change the physical structure of the code. This is a valuable ergonomic improvement — they let you group conditionals but they still require the entire implementation to live inside the same impl block.

The problem I’m trying to address is the difficulty of maintenance and navigation when that block grows too large, especially in real-world scenarios like:

  • macros generating method bodies,

  • a large number of methods,

  • cross-cutting conditionals by feature flags or target platforms.

Partial impl blocks allow those pieces to be physically separated in the source code, which improves locality of reasoning and separation of concerns, without forcing the developer to create many artificial traits purely for organizational purposes.


Even with cfg_select!, the structure is still:


impl BigTrait for MyType {    
    fn foo(&self) { … }      
    cfg_select! {         // many methods here     
    }
}

In other words:

  • Visually, it’s still a single impl block with many methods,

  • Navigation remains linear and monolithic,

  • IDEs and tools still represent it as one big block.

What partial impl blocks allow is code to be physically split across file/module boundaries, while having the compiler treat them as one implementation:


impl BigTrait for MyType { 
    /* foo */
}  
#[cfg(feature = "foo")] 
impl BigTrait for MyType { 
    /* foo methods */
}  

#[cfg(feature = "bar")] 
impl BigTrait for MyType { 
    /* bar methods */
}

Even though cfg_select! is useful to reduce repetition within a block, it doesn’t address physical organization when a single block becomes unwieldy or when macro-generated code mixes with handwritten code.


You mention multiple impl blocks in different places as a code smell — and that’s a valid concern. There should be sensible guidelines so the feature isn’t abused.

One refinement that could be added (and is worth discussing in the pre-RFC itself) is:

restrict partial impl blocks to a specific scope, such as:

  • the same module, or

  • near the type’s definition,

instead of allowing them to be scattered arbitrarily throughout the crate. That's an open question that i forgot to include in the RFC text. But what i can confirm now is that this feature would be crate-scoped or below.

That addresses readability concerns while still enabling the organizational benefits we want.

Prior art: Partial Classes and Members - C# | Microsoft Learn

Note that following that would mean that to split a block you'd need to write partial impl Foo for Bar. I think that's a good thing, especially given provided methods. Knowing that the usual "I don't see an override in this impl so it must be the provided one" doesn't hold is important, so having a (contextual) keyword as a warning would make sense.

Dunno how essential it is to allow this, though. Do you have a single example of a real-life trait you'd want to do this with? (After all, all inherent impls are already partial.)

4 Likes

Indeed, using a prefix like "partial" would make easier to read the code, and help linters and other tools at debugging these blocks. It would also respect the explicit nature of Rust code.

However using a prefix keyword for this feature has a drawback. Since it would not only make the implementation harder. but could possibly break currently running rust code. since "partial" would become a keyword.

WIth this drawback in mind, i think it's properly to set two open questions for this RFC by now:

1 - Whether or not this feature should be prefixed with partial

2- Should it be module-scoped or crate-scoped?

For the second open question, if it's module-scoped, it means that all partial impls of a given trait should be in the same module as the trait definition. This would give all benefits of this RFC, while still keeping the code-base organized. Although with some architectural changes in the projects.

If it's crate scoped, any file on a crate code-base could contain a partial impl for a given trait somewhere. This would give more flexibility for the feature. however, it could easily become a great mess. Although IDE support and guidelines could minimize this problem.

This feature could also be file-scoped, however, i think this would be a huge barrier for it's usage. and, if it should be like that, i think monolithic blocks with comments would be a better option.


And finally, the problem here is not the size of the trait(in matter of how much methods it defines). But the size(in lines of code) of the "impl" blocks that implement this trait.

Today we have, in the same impl block:

1- Core code, that will always be active. 2- Macro Generated Methods. 3- Feature Specific Code 4- Platform Specific Code

For example, today we have, in the same file:

// Trait Definition
// Struct Definition

impl SomeTrait for SomeStruct {

	macro_call_1!(...);
	macro_call_2!(...);
	// Each one of these would have 
	fn core_method_1() -> () {
	
	};
	fn core_method_2() -> () {
	
	};
	fn core_method_N() -> () {
	
	};
	
	// Cfg Select could also be used here, however, the problem that i am attaining to would still persist.
	#[cfg(target_os="windows")]
	fn platform_specific_method() -> () {
		// Dozens, maybe hundreds of lines.
	};
	
	#[cfg(target_os="linux")]
	fn platform_specific_method() -> () {
		// Dozens, maybe hundreds of lines.
	};
	
	#[cfg(feature="foo")]
	fn feature_specific_method_1() -> () {
		// Dozens, maybe hundreds of line
	}
}

With this feature. something like this Could be possible:

Consider this project structure:

root/
	src/
		module/
			mod.rs  // Trait and 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
		// ...	
		main.rs
	cargo.toml
	// ...

mod.rs:

pub trait SomeTrait {
	// Dozens Of methods, but honestly, this is not the problem even considering default implementations.
}

pub struct SomeStruct {
	
}

pub mod core_impl;
#[cfg(target_os="windows")]
pub mod windows_impl;
#[cfg(target_os="linux")]
pub mod linux_impl;
#[cfg(feature="feature_1")]
pub mod feature_1_impl;
#[cfg(feature="feature_2")]
pub mod feature_2_impl;

core_methods.rs:

use crate::module::SomeTrait;
use crate::module::SomeStruct;

impl SomeTrait for SomeStruct {
	fn core_method_1() -> () {};
	fn core_method_2() -> () {};
	fn core_method_N() -> () {};
}

windows_impl.rs:

use crate::module::SomeTrait;
use crate::module::SomeStruct;

// This CFG is redundant, but could exist for documentation or style purposes.
#[cfg(target_os="windows")]
impl SomeTrait for SomeStruct {
	fn platform_specific_method_1() -> () {};
	fn platform_specific_method_2() -> () {};
	fn platform_specific_method_N() -> () {};
}

linux_impl.rs:

use crate::module::SomeTrait;
use crate::module::SomeStruct;

// This CFG is redundant, but could exist for documentation or style purposes.
#[cfg(target_os="linux")]
impl SomeTrait for SomeStruct {
	fn platform_specific_method_1() -> () {};
	fn platform_specific_method_2() -> () {};
	fn platform_specific_method_N() -> () {};
}

feature_1_impl.rs

use crate::module::SomeTrait;
use crate::module::SomeStruct;

// This CFG is redundant, but could exist for documentation or style purposes.
#[cfg(feature="feature_1")]
impl SomeTrait for SomeStruct {
	fn feature_1_specific_method_1() -> () {};
	fn feature_1_specific_method_2() -> () {};
	fn feature_1_specific_method_N() -> () {};
}

feature_2_impl.rs

use crate::module::SomeTrait;
use crate::module::SomeStruct;

// This CFG is redundant, but could exist for documentation or style purposes.
#[cfg(feature="feature_2")]
impl SomeTrait for SomeStruct {
	fn feature_2_specific_method_1() -> () {};
	fn feature_2_specific_method_2() -> () {};
	fn feature_2_specific_method_N() -> () {};
}

Also, all of these methods could have the exactly same purpose, and even share the same name. Just toggled on/off by cfg to avoid conflicts. But still have different, well organized, and documented implementations in separated file.

And finally In this example, it's my personal project that i've been taking for about a year by now. It's a DSL for a Text Processor. It's has some problems through it's code since i've started it few weeks after i started learning Rust:

For a general Context, That project propose scripts like these for describing text transformation pipelines:

The rule here is simple, one instruction per line that makes a single transformation to a given input string, each instruction is a resumed english phrase that describes the operation:

atb "foo"; // Add to beginning
dlf; // Delete First
raw "oo" "bar"; // Replace All With

For example, if the input is "banana", the output of this script will be: "barbanana".

Each instruction is then parsed with crates like shell_words to objects like these:


// Replace Nth with
impl TokenMethods for Rnw {

	fn to_atp_line(&self) -> Cow<'static, str> {
		format!(
		"rnw {} {} {};\n",
		self.pattern,
		self.text_to_replace,
		self.index).into()
	}

	fn parse(&self, input: &str) -> Result<String, AtpError> {
		let mut count = 0;
		let mut idx = None;
		for m in self.pattern.find_iter(input) {
			if count == self.index {
				idx = Some((m.start(), m.end()));
				
				break;
			}		
			count += 1;
		}
	
		if let Some((start, end)) = idx {
		
			let mut result = 
				String::with_capacity(	
					input.len() - (end - start) + self.text_to_replace.len()
				);
			result.push_str(&input[..start]);
			result.push_str(&self.text_to_replace);
			result.push_str(&input[end..]);
			
			return Ok(result);
		
		}
	
		Ok(input.to_string())
	
	}

	fn from_vec_params(&mut self, line: Vec<String>) -> Result<(), AtpError> {
	
		// "rnw {pattern} {text_to_replace} {index};"
			
		if line[0] == "rnw" {	
			self.pattern = Regex::new(&line[1]).map_err(|_|
			
			AtpError::new(
			
			AtpErrorCode::TextParsingError(
				"Failed creating regex".into()
			),
			
			line[0].to_string(),
			
			line.join(" ")
			
			)
			
			)?;
			
			self.text_to_replace = line[2].clone();
			
			self.index = string_to_usize(&line[3])?;
			
			return Ok(());
		}
		
		Err(	
			AtpError::new(	
				AtpErrorCode::TokenNotFound(
					"Invalid parser for this token".into()
				),			
				line[0].to_string(),	
				line.join(" ")	
			)
		)
	
	}

	fn get_string_repr(&self) -> &'static str {
		"rnw"
	}
	
	#[cfg(feature = "bytecode")]
	fn get_opcode(&self) -> u32 {
		0x1f
	}

	#[cfg(feature = "bytecode")]
	fn from_params(
		&mut self,
		instruction: &Vec<AtpParamTypes>
	 ) -> Result<(), AtpError> {
	
		if instruction.len() != 3 {
	
			return Err(
				AtpError::new(
					AtpErrorCode::BytecodeNotFound(
						"Invalid Parser for this token".into()
					),
					"",
					""
				)
			);
		}
	
		match &instruction[0] {
			AtpParamTypes::String(payload) => {
				self.pattern = Regex::new(&payload.clone()).map_err(|_|
					AtpError::new(
						AtpErrorCode::TextParsingError(
							"Failed to create regex".into()
						),
						"sslt",
						payload.clone()
					)
				)?;
	
			}
			_ => {
			
				return Err(
					AtpError::new(
						AtpErrorCode::InvalidParameters(
							"This token takes a single String as argument".into()
						),
					"",
					""	
					)
				);
			
			}
	
		}
	
		match &instruction[1] {
			AtpParamTypes::String(payload) => {
			self.text_to_replace = payload.to_string();
		
			}
			_ => {
			
				return Err(
					AtpError::new(
						AtpErrorCode::InvalidParameters(
							"This token takes a single String as argument".into()
						),	
						"",	
						""
					)
				);
			
			}
		
		}
		
		match &instruction[2] {
			AtpParamTypes::Usize(payload) => {
				self.index = payload.clone();
			}
		
			_ => {
				return Err(	
					AtpError::new(
						AtpErrorCode::InvalidParameters(
							"This token takes a single usize as argument".into()
						),
					"",
					""	
					)
				);
			}
		}
		return Ok(());
	
	}

	#[cfg(feature = "bytecode")]
	fn to_bytecode(&self) -> Vec<u8> {
	
		let mut result = Vec::new();
		let instruction_type: u32 = self.get_opcode() as u32;
		
		let first_param_type: u32 = 0x01;
		let first_param_payload = self.pattern.as_str().as_bytes();
		let first_param_payload_size: u32 = first_param_payload.len() as u32;
		let first_param_total_size: u64 = 4 + 4 + (first_param_payload_size as u64);
		
		let second_param_type: u32 = 0x01;
		let second_param_payload = self.text_to_replace.as_bytes();
		let second_param_payload_size: u32 = second_param_payload.len() as u32;
		let second_param_total_size: u64 = 4 + 4 + (second_param_payload_size as u64);
		
		let third_param_type: u32 = 0x02;
		let third_param_payload = (self.index as u32).to_be_bytes();
		let third_param_payload_size: u32 = third_param_payload.len() as u32;
		let third_param_total_size: u64 = 4 + 4 + (third_param_payload_size as u64);
		let instruction_total_size: u64 =
		
		8 + 4 + 1 + first_param_total_size + second_param_total_size + third_param_total_size;
		
		// Instruction Total Size
		result.extend_from_slice(&instruction_total_size.to_be_bytes());
		
		// Instruction Type	
		result.extend_from_slice(&instruction_type.to_be_bytes());
		
		// Param Count	
		result.push(2);
		
		// First Param Total Size	
		result.extend_from_slice(&first_param_total_size.to_be_bytes());
		
		// First Param Type	
		result.extend_from_slice(&first_param_type.to_be_bytes());
		
		// First Param Payload Size	
		result.extend_from_slice(&first_param_payload_size.to_be_bytes());
		
		// First Param Payload	
		result.extend_from_slice(&first_param_payload);
		
		// Second Param Total Size
		result.extend_from_slice(&second_param_total_size.to_be_bytes());
		
		// Second Param Type	
		result.extend_from_slice(&second_param_type.to_be_bytes());
		
		// Second Param Payload Size	
		result.extend_from_slice(&second_param_payload_size.to_be_bytes());
		
		// Second Param Payload
		result.extend_from_slice(&second_param_payload);
		
		// Third Param Total Size	
		result.extend_from_slice(&third_param_total_size.to_be_bytes());
		
		// Third Param Type
		result.extend_from_slice(&third_param_type.to_be_bytes());
		
		// Third Param Payload Size
		result.extend_from_slice(&third_param_payload_size.to_be_bytes());
		
		// Third Param Payload
		result.extend_from_slice(&third_param_payload);
				
		result
	
	}

}

These objects are the core of the project, and as you can see with the above example, i'm currently forced to group all methods on the same impl block.

In this same impl block we have:

1- Core methods, like .parse(), .to_atp_line() and

2- Methods that should be macro-generated like .get_opcode() and .get_string_repr()

3- Feature specific methods annotated with cfg, like .from_params()and .to_bytecode(),

This project is very simple by now, however. If it become serious. platform specific and feature specific code for this same trait will grow a lot. Just as macro generated code and core methods.

I've though about ways to split this in simpler traits as you can check in the GIT historic. however, this just made managing these objects around the project harder, since, Rust has no inheritance as other languages do. I've found myself needing to do a lot of workarounds(like reparsing to convert between traits) just because i wished to organize my project better. And even after learning a lot about Rust after the initial implementation, the problem still persist.

I could also use macros to solve this problem(It would probably solve). However, This would not discard the RFC completely, mostly because partial impl blocks pair very well with macros and conditional coding. It's not only about organization after all.

For such a big project, which has currently over 50 instructions. Organization is a must have. I have plans to turn this project open-source in the future, so it should be easy to understand. And people could create new instructions just by implementing this trait and modifying fewer files.

The idea of this was not to promote my project, however, the discussion eventually took us here. That's the project link

Well, i think that's it for this post. i think i covered everything.

1 Like

Welp, here is another LLM-driven RFC thread.

6 Likes

One way I've handled the "huge function in a trait impl" case, sometimes, is to pull the body out into a helper and make the function in the trait impl just a line or two calling that helper.

Especially since you can always add a new inherent impl block wherever you want, with a private method that you use to implement the trait, making the trait impl short.

2 Likes

Well, in some way you're correct. I use AI to revise some posts and replies. since english is not my native language. however, it's just for refining. I can Guarantee that the brute text is human written.

Three ideas (#1 doesn't play that nice with #2 and #3 - though they are not mutually exclusive) related to how this can work with macros.

  1. If this is added, I think #[derive(...)] should be expanded to support impl Trait items. That way, the macro can generate the more gruesome parts of the trait implementation in a separate block.

    This can currently be done with a regular attribute macro (e.g. - typetag), but derives are better because they promise not to alter the annotated item itself, which greatly improves the rust-analyzer experience.

  2. It should be possible to mark items in a partial impl block with something like #[overwritable]. If another impl block defines the same item - the one without #[overwritable] will be used, and the #[overwritable] one will be ignored. But if there is only #[overwritable] version, that's the one which will be used.

    The idea here is that a proc macro (probably a derive on the type itself. One from my previous suggestion in this comment won't need it) will be able to make some of the things in the impl block it generates #[overwritable], and the non-macro impl block will either leave them as is or provide its own version. This will be like default implementation in trait definition blocks - except the derive macro will have knowledge on the structure of the type, so it could create more tailored ones.

  3. Instead of requiring equivalent headers, how about binding the first header to a name and reusing that name in the other blocks? Something like this (very stupid syntax, feel free to bikeshed):

    impl<Q, R, S, T> Foo = Bar<Q, R> for Baz<S, T>
    where
        Q: I<Am = R> + Trying<S>,
        R: To<S> + Make<T> + This<Q>,
        S: Impl<T> + Block<Q> + As<R>,
        T: Complex<Q> + As<R> + Possible<S>,
    {
        // First partial impl block
    }
    
    impl Foo as {
        // Second partial impl block
    }
    

    Here too, the idea is that a macro can generate the first, complex block using knowledge from Barz' definition, and then the user won't need that heavy signature - they'd just need Foo.

    Note that this (or some other syntax that would a name) also resolves some problems that were mentioned here:

    • Identifying that an impl block is partial - we get this automatically, since they now have a different syntax.
    • Scope restriction - since we are binding a name, we can add things like pub(crate) impl Foo = Bar for Baz. Of course - pub should not be possible here.
1 Like

Derives are definitely the most concretely motivated use case for me. For all the abstract “organization” ideas, I’d like to see some real code, how you’d like to organize it, and what you’re doing to work around that instead…but derives immediately give me “oh, yeah, it’s reasonable to have a derive that does 3/4 of the requirements and then you only have to fill in the fourth”. If you want to do that today, you need to put the extra implementation inside the macro too, or implement it standalone and provide the name of the function (like Josh’s workaround), or detach the macro from the type (but then you have to repeat more information). None of those are as clean as derive(MostOfMyTrait).

1 Like

Since the inspiration is C#, the motivation there (from what I remember - haven't touched it in over a decade) is mostly to have one tool-maintained file and one human-maintained file without the human directly editing the former or the tool modifying the latter.

Classic use case - the UI designer modifying a file that defines the visual layout in code, and the human programmer edits another file implementing the behavior of that UI. Both files use partial to implement the same class.

Believe it or not, I've never written a single line of code in C#.

I researched this because it was mentioned in this chat and I found it quite interesting. It's very good to know this.

Not that being inspired by another language is necessarily bad, but I came to this conclusion on my own while programming in Rust. If it weren't for the time difference, I would be the inventor of this.

In this case, From what i understood of your ideia, it's like a variable for the header of the impl block. This could be a good idea in bigger projects, and maybe a possible subject for another, but simpler RFC.

Although this idea pairs very well with partial impl blocks, i do not think it's a good idea to bundle it in a single RFC.

About the #[overwritable] annotation, my fear about this kind of solution is a need to implement a priority mechanism for the compiler to decide which partial impl block is valid in the case two implementations for the same method live at the code at the same time. Without better developing, this would just make the code harder to read and write.

Also, you have said about a way to identify these partial impl blocks. It was suggested before to use the partial prefix in a example like this:


use crate::module::SomeTrait;
use crate::module::SomeStruct;

#[cfg(target_os="windows")]
partial impl SomeTrait for SomeStruct {
	fn platform_specific_method_1() -> () {};
	fn platform_specific_method_2() -> () {};
	fn platform_specific_method_N() -> () {};
}

That's a good idea, however i will still keep as an open question by now due to the drawbacks of this solution that i have described in a previous post. It would be friendly to beginners and used to make Rust closer to other languages in future lectures.

Another way to identify these partial impl blocks is through annotations, like #[partial], simple, direct, and easy to type. And would not have the same drawbacks as a new keyword. that's something to consider in the future.

The original concept of this idea was to be a niche feature. where beginners would pass months programming in rust never knowing the existence of it. However, when the dev need it, it would be there to save the day.

No priorities - if two implementation of the same item use #[overwritable] just treat it as a conflict.

The idea is that only macros will write #[overwritable], and typically there should only be one macro implementing a specific trait on a specific type.

Human-written code does not need #[overwritable] because even if you split it for organizational reasons you can just remove the colliding item in one of the implementations.

It's a pretty interesting idea looking by that way.

A possible effect of this solution would be traits without default implementations. This is already possible in the current Rust and pretty common.

In the macro example, the trait would only be used for documentation purposes and defining the contract, more like as a summary to navigate through the code with a little help of the IDE.

By re-analyzing this idea, another good point about it is that this solution is granular, some impl blocks could be overwritten, some not. this would require some prior knowledge of the developer on whether or not should the partial impl block be #[overwritable] or not, since it would require the dev to know in advance if that code will not be sufficient for all use-cases. but honestly. it wouldn't be a problem with tools like GIT out there.

This idea alone can become pretty powerful, almost an impl inheritage, just by listing all utilities we have for this concept by now:

1- Code organization: Pretty abstract, but would help a lot in larger code bases

2- Macros implementing part of a trait: A good point in favor of the concept. The human written code would live in another partial impl block

3- Sinergy with conditional coding through #[cfg]: It would become easier to split the project along multiple files when using features and platform specific code.

4- Macros generating default implementations with #[overwritable]: Would help making trait definitions even more cleaner and most for doc purpose(rarely trait definitions are a mess, but would be an effect). Not only that, but would allow for the dev to overwrite the automatic code when needed.

I did not suggest #[overwritable] to replace default implementations. Macros have lots of overhead - on the programmer as well as on the compiler - and should not be used for things that can so easily be achieved without them. These are for cases where default implementations cannot be used because such implementation would require information on the type's structure.

Indeed, but as i implictly stated in the previous post , that's an side-effect that should be considered, in some scenarios, it could and it will be used like that.

I don't think there's any downside to making the syntax partial impl, because it doesn't require partial to be a keyword – I don't think there's any situation in Rust where you can currently write the tokens partial impl in that sequence, so you just define that specific sequence of tokens as being keyword-like rather than making partial a keyword on its own. (This is sometimes called a "contextual keyword".) A good existing example of this sort of thing is &raw const – this specific sequence of three tokens was given a meaning recently (which was backwards-compatible because that sequence could never have appeared in a valid Rust program except as a macro argument), but raw is not a keyword and you can legally use it as, e..g, a function or variable name.

1 Like