RFC: Selectable Trait Implementations via Impl Modules and Implementation Arguments
- Feature name: impl_modules
- Status: Draft proposal
- Scope: Language design only (not implementation)
Summary
This RFC proposes an opt-in mechanism for using alternate trait implementations (including implementations that would be forbidden by today’s orphan rules) by making the chosen implementation explicit in the type.
The proposal adds:
- Impl modules: a module may export one or more public, selectable trait impls using pub impl Trait for Type { ... }.
- Implementation arguments: types may carry an implementation signature by listing one or more impl module “selectors” as additional generic arguments:
MyType<T, U, other_crate::other_module::Deref>
- Explicit impl selection casts: values can be retyped to use a selected impl via as:
let y = x as MyType<T, U, other_crate::other_module::Deref>;
Key properties:
- If implementation arguments are omitted, Rust behaves exactly as today (ordinary coherence and orphan rules).
- Types that differ only by implementation arguments are distinct types (no implicit mixing).
- Alternate impl selection is designed to work best with generic code and trait bounds, and it intentionally avoids “impl erasure”.
This preserves Rust’s “no spooky action at a distance” principle while enabling explicit, local use of alternate impls.
Motivation
Rust’s orphan rules prevent implementing a foreign trait for a foreign type, for good reasons (coherence, ecosystem stability, and predictability). However, the restriction forces common patterns into awkward workarounds:
- newtype wrappers that forward constructors/fields/methods
- adapter traits
- helper wrapper crates that explode API surface and cause “type fragmentation”
These workarounds have costs:
- boilerplate and cognitive overhead
- wrapper types that are incompatible with APIs expecting the original type
- significant friction when the “alternate behaviour” is truly orthogonal (e.g., alternate Deref, Hash, Borrow, AsRef semantics used for a specific subsystem)
This RFC aims to retain Rust’s core coherence benefits while enabling alternate impls only when explicitly selected by the user.
Guide-level explanation
Overview
The core idea is:
- A crate can define an alternate implementation of some trait for some type in a named module.
- Code that wants that alternate behavior uses a type annotation that names the impl module.
- A value can be converted between “implementation signatures” using an explicit cast.
- If you don’t opt in (no implementation arguments), nothing changes: normal orphan rules and coherence apply.
Defining an alternate impl in an impl module
Consider a type Utf8Buf that stores UTF‑8 data:
// crate: utf8_buf
pub struct Utf8Buf(Vec<u8>);
impl Utf8Buf {
pub fn new(s: &str) -> Self { Self(s.as_bytes().to_vec()) }
pub fn as_str(&self) -> &str { /* ... */ }
}
A downstream crate wants Utf8Buf to behave like a &str via Deref, but does not want to force wrapper types everywhere.
With this RFC it can export an impl module:
// crate: utf8_deref
pub mod str_view {
pub impl std::ops::Deref for utf8_buf::Utf8Buf {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
}
Notes:
- pub impl marks this as a selectable implementation.
- The impl lives in a module (utf8_deref::str_view) so it has a stable path.
- This pub impl is not considered by ordinary trait resolution unless explicitly selected.
Selecting an alternate impl at the type level
Callers opt in by writing an implementation argument:
use utf8_buf::Utf8Buf;
type Utf8BufStr = Utf8Buf<utf8_deref::str_view::Deref>;
Now Utf8BufStr behaves like a Deref<Target = str> type.
Selecting an impl for a value
Given a normal Utf8Buf, you can select the alternate impl explicitly:
let b = Utf8Buf::new("hello");
let s = b as Utf8Buf<utf8_deref::str_view::Deref>;
// `s` now has the alternate `Deref` implementation in scope for trait solving.
The cast is intended to be:
- infallible
- zero-cost
- a change of compile-time “implementation signature” only
Method resolution and deref coercions use the selected impl
Because Deref affects operator * and autoderef method lookup, selection becomes immediately useful:
use std::ops::Deref;
fn len_via_deref<T>(x: T) -> usize
where
T: Deref<Target = str>,
{
x.len()
}
let b = Utf8Buf::new("hello");
let s = b as Utf8Buf<utf8_deref::str_view::Deref>;
assert_eq!(len_via_deref(s), 5);
No implicit mixing with “concrete” types
A deliberate restriction: types with implementation arguments are distinct from their base type.
So this is rejected:
fn takes_plain(_: Utf8Buf) {}
let b = Utf8Buf::new("hello");
let s = b as Utf8Buf<utf8_deref::str_view::Deref>;
takes_plain(s); // error: expected Utf8Buf, found Utf8Buf<...Deref>
Why this restriction exists:
- Deref has an associated type Target.
- Allowing “impl-erased passing” would make associated types ambiguous (and can introduce semver hazards when function bodies change).
- Requiring explicit casts keeps behavior changes explicit.
If needed, the caller can cast back:
takes_plain(s as Utf8Buf);
Multiple selections compose with commas
A type may select multiple traits:
type SpecialBuf = Utf8Buf<
utf8_deref::str_view::Deref,
protocol_hash::v1::Hash,
>;
Each selection affects only the specified trait.
Generic function requiring a specific trait implementation
Sometimes you want a function to accept a family of types while pinning a specific Trait impl. This RFC adds a special syntax T:= Impl which can be used for this purpose. (Alternatively we could specify T: Trait via Impl if the via keyword is preferred by the community)
Example alternate Hash (simplified):
// crate: protocol_hash
pub mod v1 {
pub impl std::hash::Hash for utf8_buf::Utf8Buf {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
// Example behavior: hash lowercased bytes for case-insensitive protocol IDs
for b in self.as_str().bytes() {
state.write_u8(b.to_ascii_lowercase());
}
}
}
}
Now a function can require that specific Hash impl:
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
fn protocol_stable_hash<T>(key: &T) -> u64
where
// Here we specify that we only accept the canonical Hash impl
// Alternatively we could select a selectable impl with
// T:= protocol_hash::v1::Hash,
T:= std::hash::Hash,
{
let mut h = DefaultHasher::new();
key.hash(&mut h);
h.finish()
}
fn hash_protocol_id(id: &utf8_buf::Utf8Buf<protocol_hash::v1::Hash>) -> u64 {
protocol_stable_hash(id) // This results in an error since we're trying to use a different Hash impl
}
Reference-level explanation
Terms
- Base type: a type without any implementation arguments, e.g. Utf8Buf or MyType<T, U>.
- Implementation argument: an additional argument in a type’s angle-bracket list that refers to an exported selectable impl in an impl module (a selector).
- Implementation signature: the set of selected traits and their associated impl module selectors attached to a type.
Impl modules and selectable impls
This RFC introduces pub impl blocks:
pub mod m {
pub impl Trait for Type { ... }
}
Semantics:
- A pub impl Trait for Type defines a selectable implementation of Trait for Type.
- Selectable impls are not considered by ordinary trait resolution unless explicitly selected via implementation arguments.
- A selectable impl is identified by the module path and trait name, e.g. other_crate:
:TraitName.
Overlap rule within a module
Within a single module, for a given trait TraitName, the set of pub impl TraitName for ... must be non-overlapping under the same overlap rules Rust uses today for ordinary impl coherence.
- Multiple impls are permitted if they are disjoint.
- Specialization-style overlap (e.g., blanket + specific) is disallowed within the same module.
This restriction is chosen to keep selection deterministic and to avoid importing specialization complexity into the initial design.
Type syntax and parsing
A type path may include implementation arguments after its declared generic parameters:
MyType<A, B, other_crate::m::Deref, other_crate::n::Hash>
Rules:
- Let MyType declare N generic parameters (possibly 0).
- The first N arguments are interpreted as normal generic arguments as today.
- Any remaining arguments are interpreted as implementation arguments and must resolve to selectable impl identifiers of the form path::TraitName.
If a “remaining argument” does not resolve to a selectable impl identifier, it is an error.
This allows non-generic types to carry implementation arguments, e.g.:
u8<custom_stringify::v1::Stringify>
Trait resolution
When solving an obligation T: Trait for a type T that may carry an implementation signature:
- If T has an explicit selection for Trait, trait resolution must use the selected pub impl for Trait from the referenced impl module.
- Otherwise, trait resolution proceeds exactly as in Rust today (ordinary coherence/orphan rules).
This is an explicit “selected impl wins” rule and is the primary mechanism that allows foreign trait / foreign type impls without affecting existing code.
Type identity and equality
Two types are considered distinct if they differ in their implementation signature, even if they share the same base type and normal generic arguments:
- Utf8Buf ≠ Utf8Buf<utf8_deref::str_view::Deref>
- MyType<T, U, a::Hash> ≠ MyType<T, U, b::Hash>
Consequences:
- They cannot be compared directly without conversion.
- They cannot be mixed in homogeneous collections without explicit erasure/wrapping.
- Generic inference treats them as different types.
Conversions with as
This RFC adds a new class of as cast: an implementation-signature cast.
A value of type Base<…> may be cast to Base<…, selectors…> and vice versa, provided:
- the base type and ordinary generic arguments match exactly
- every selector listed is applicable to the base type instantiation (i.e., the module exports a matching pub impl)
- no trait is selected more than once
The cast is:
- infallible
- zero-cost
- representation-preserving
This cast is also permitted for references and pointers, changing only the pointee’s implementation signature.
TypeId
TypeId includes the implementation signature. Therefore:
- TypeId::of::() != TypeId::of::<Utf8Buf<utf8_deref::str_view::Deref>>()
This is necessary to avoid unsound or misleading type-map / Any behavior where two behavior-distinct types would share the same TypeId.
Non-selectable traits
This RFC initially forbids selecting implementations for:
- auto traits (e.g., Send, Sync, Unpin)
- other compiler-defined “auto-like” traits if applicable
Rationale: auto traits are deeply tied to soundness and optimization assumptions; treating them as selectable would require a much more careful model.
Tooling and diagnostics requirements
Because implementation signatures can change behavior substantially (especially for Deref), tooling should surface them prominently:
- Error messages for trait obligations should show the selected impl module path.
- “Go to definition” for trait methods should navigate to the selected impl when present.
- Hover should include the implementation signature (or a compact summary).
- Formatting conventions should keep multi-selection types readable (rustfmt rules).
Drawbacks
- Type proliferation Each distinct implementation signature yields a distinct type, increasing monomorphization and type-level complexity.
- API friction at concrete boundaries Since selected types do not implicitly coerce to base types, APIs that want to accept both must use explicit casts or choose one signature.
- Behavior changes can ripple Changing one selected trait (e.g., Deref) can indirectly affect behavior in other trait impls and generic code due to method resolution and blanket impls.
- Language and tooling complexity Adds new surface syntax (pub impl, implementation arguments, impl-selection cast) and requires diagnostics/IDE support to stay understandable.
Rationale and alternatives
Why not newtypes
Newtypes already provide a way to define alternate impls, but they:
- fragment the type namespace (MyType vs MyTypeWrapper)
- require boilerplate forwarding
- often don’t integrate smoothly with APIs expecting the original type
This RFC aims to achieve “newtype-like alternate impl power” while keeping the nominal type and field/inherent-method usage intact.
Why not scoped impls via imports
Implicit scoping (e.g., “whichever impl is imported wins”) tends to create:
- spooky action at a distance
- brittle refactors
- hard-to-debug trait solver changes
This RFC requires explicit selection in the type to keep behavior local and stable.
Prior art
- Haskell typeclass instances and explicit dictionary passing (conceptual similarity)
- Scala implicits / typeclass instances (illustrates the hazards of implicit resolution)
- Rust newtype pattern (closest existing workaround)
- Prior Rust discussions on relaxing coherence with explicit selection (conceptual neighborhood)
Unresolved questions
- Naming and stability Should pub impl allow explicit naming / aliasing to avoid collisions or improve ergonomics?
- Visibility and re-exports Exact rules for re-exporting pub impl selectors and how docs present them.
- Interaction with specialization The RFC forbids overlap within a module initially. Future work could consider optional specialization within modules.
- Error message design Standard format for printing implementation signatures in diagnostics.
- Trait objects and erasure patterns How best to guide users when they need to store different signatures together (likely via trait objects or enums).
Future possibilities
- Controlled support for selecting some non-auto marker traits with stronger constraints.
- A first-class “impl signature parameter” to make “accept MyType regardless of impl signature” ergonomic without casts.
Appendix: Complete Deref-based example
Base crate
// utf8_buf
pub struct Utf8Buf(Vec<u8>);
impl Utf8Buf {
pub fn new(s: &str) -> Self { Self(s.as_bytes().to_vec()) }
pub fn as_str(&self) -> &str { /* ... */ }
}
Alternate Deref impl module
// utf8_deref
pub mod str_view {
pub impl std::ops::Deref for utf8_buf::Utf8Buf {
type Target = str;
fn deref(&self) -> &str { self.as_str() }
}
}
Use
use std::ops::Deref;
use utf8_buf::Utf8Buf;
fn len_via_deref<T>(x: T) -> usize
where
T: Deref<Target = str>,
{
x.len()
}
let b = Utf8Buf::new("hello");
let s = b as Utf8Buf<utf8_deref::str_view::Deref>;
assert_eq!(len_via_deref(s), 5);
This demonstrates the intended ergonomic payoff: alternate behavior becomes available to generic code without wrapper types, while remaining explicit and type-directed.