[Pre-RFC] Selectable Trait Implementations

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:

  1. Impl modules: a module may export one or more public, selectable trait impls using pub impl Trait for Type { ... }.
  2. 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>
  1. 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::m::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:

  1. Let MyType declare N generic parameters (possibly 0).
  2. The first N arguments are interpreted as normal generic arguments as today.
  3. 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:

  1. If T has an explicit selection for Trait, trait resolution must use the selected pub impl for Trait from the referenced impl module.
  2. 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

  1. Type proliferation Each distinct implementation signature yields a distinct type, increasing monomorphization and type-level complexity.
  2. 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.
  3. 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.
  4. 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

  1. Naming and stability Should pub impl allow explicit naming / aliasing to avoid collisions or improve ergonomics?
  2. Visibility and re-exports Exact rules for re-exporting pub impl selectors and how docs present them.
  3. Interaction with specialization The RFC forbids overlap within a module initially. Future work could consider optional specialization within modules.
  4. Error message design Standard format for printing implementation signatures in diagnostics.
  5. 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.

3 Likes

Haven't gotten in deep on this yet, but there are some relevant properties to consider in the design of a semantics for modular implicits (the academic term for the concept being proposed here).

The COCHIS paper opens with a solid introduction to the relevant semantic guarantees and prior art.

In short, there are two key properties to assess in the design of a modular implicit system:

  • Coherence: a valid program must have exactly one meaning (there can't be ambiguity about what implicit [trait impl, in Rust parlance] is selected at a given site).
  • Stability: all possible elaborations of a program have the same meaning (practically, this means inferring a more-specific type for a value should never result in a different implicit being selected).

If you haven't already, I highly recommend checking out the paper to see a design for modular implicits which achieves both coherence and stability.

2 Likes

The big alternative that I think needs to be considered here is something like an explicit "comparer" generic that, in a sense, "carries" the different impls.

Suppose you had, for example, BinaryHeap<T, Comparer = DefaultComparer>. Then the bound on the methods would not longer directly be T: Ord, but rather would be Self::Comparer: Comparer<T>. (That trait would be something like trait Comparer<T> { fn cmp(lhs: &T, rhs: &T) -> Ordering; }.)

That means you can change from BinaryHeap<u32> to BinaryHeap<u32, ReversedComparer> without needing to touch your push/pop calls or how you consume it from an iterator or ...


How would your proposal spell that? BinaryHeap<u32<mycrate::other_module::Ord, mycrate::other_module::PartialOrd, mycrate::other_module::PartialEq, mycrate::other_module::Eq>>?

3 Likes

Note: I figured out my concerns below. Leaving them as the thoughts might be useful to others. The answer to part of the concern is the requirement of pub impl (Trait for) Type, not just any impl. However, adding an entirely new impl could still be unsound; there’d need to be a marker for “this type does not impl Foo by default, but anyone may soundly provide an impl of Foo”. Else, traits and methods not explicitly mentioned in pub impl blocks could simply be prohibited from being overridden or newly implemented.

Initial reaction

I’m slightly concerned about whether this would undermine the assumptions of unsafe code. E.g., if you make a version of Rc with a modified Deref or DerefMut impl, how does that interact with Pin? And even if there were a rule that you cannot change an existing impl (which would bring back orphan rule problems, but that’s besides the point), I remember some unsound interaction between CoerceUnsized, Pin, and Deref involving finding a #[fundamental] type which did not implement DerefMut so that a pathological DerefMut impl could be created, or something along those lines.

If Rc::<T, my_impls::DerefMut>::pin may have to be prohibited (noting that Rc::pin is a safe function), I don’t think that types with custom trait impls can, in general, use the original types’ methods. That would make it no better than a newtype.

I suppose that there could be an marker that a base type could use to opt-in to the ability to have its trait impls overridden; that base type would have to promise that none of its unsafe code relies on the behavior of its trait impls.

More thoughts

And, after reading more carefully, there is a marker. Doh. unsafe code could then take responsibility for not relying on those pub impls for safety, and changing a private impl to a pub impl would be a breaking change.

This implies that the standard library’s types generally cannot have pub impls; it’s too late, and unsafe code already relies on the behavior of core/alloc/std.

I think that the Ord impl (and friends) of u32 cannot be soundly overridden at all.

As an aside, I prefer to have my comparator generics take a &self argument. If the generic is a ZST, that argument should boil away, but it leaves open the option of choosing &dyn Comparer (or an enum, or similar) to decide the sorting at runtime.

Note that I intentionally didn't do that, because I don't know what it means to have different instances of a comparer for the two sides in BTreeSet: Eq, for example.

In the abstract I agree, though. Would be nice to see what a fix for that would look like.

Also, what about things like BinaryHeap where changing the sorting at runtime messes up the invariants?

Another thing that should be described: how would type inference work with this?

Personally, I'd expect an exact implementation to be inferred only when the context requires one explicitly-specified implementation, so this would pass:

struct Foo(u32);
pub mod foo_hash { pub impl Hash for Foo { .. } }
pub mod decoy_impl { pub impl Hash for Foo { .. } }
fn takes_foo_hash(foo: Foo<foo_hash::Hash>) { .. }
takes_foo_hash(Foo(5)); // successfully infers here
let foo: Foo = Foo(5);
takes_foo_hash(foo as _); // also successfully infers here

but this would fail, even if the compiler knows this is the only reachable implementation:

struct Foo(u32);
pub mod foo_hash { pub impl Hash for Foo { .. } }
fn takes_foo_hash(foo: impl Hash) { .. }
takes_foo_hash(Foo(5)); // can't infer Foo<foo_hash::Hash> for the argument here
let foo = Foo(5);
takes_foo_hash(foo as _); // still can't infer the argument here

But idk how that tracks with everyone else's expectations. I could see people reasonably objecting to my first code snippet working.

I meant that, for each particular value of a collection type, you'd make one choice of comparator for that collection. So you would never change the sorting of an individual BinaryHeap at runtime, but you could decide (at runtime) on creation of the BinaryHeap how to sort it. (It doesn't seem to introduce new opportunities for odd behavior, given that someone could already use a static variable, rand, or something similar to get odd "comparers". I'd just rely on the user to promise that their comparator is well behaved and doesn't abuse internal mutability, on pain of panics.)

As for BTreeSet: Eq, it looks like it ultimately comes down to BTreeMap's implementation of PartialEq: self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a == b). I think there could just be a Comparer: PartialEq<OtherComparer> (or Comparer: Eq) bound added, and a self.comparer() == other.comparer() test. Perhaps there could also then be an equal_modulo_comparers-ish method (which would need to be more involved), but I assume that the PartialEq/Eq implementation should (in general) take the comparers into account, since they affect visible behavior (iteration order). BTreeSet<T> already does not implement PartialEq<BTreeSet<U>> even if T: PartialEq<U>, so this seems fair.

I don't really care how it's implemented today; I care about how I'd think about it. I like that BTreeSet<u32> == BTreeSet<Reverse<u32>> doesn't compile, because it's probably not going to do anything useful. So I also like that BTreeSet<u32> == BTreeSet<u32, Reversed> wouldn't compile.

I guess you can always say "well, don't do dumb things" with custom comparers, though. It's not obvious to me when I'd want a runtime comparer in my BinaryHeap, though.

2 Likes

I largely agree that I don't think I'll want/need it, but since the additional hassle (on top of providing a comparator generic) seems negligible to me, I'm willing to throw in a &self for free in my libraries.

I've actually seen the annoying opposite in a few libraries: using only dyn Comparer, presumably in order to avoid propagating a generic parameter everywhere. I don't know how bad the cost of using that in a hot loop is, but I strongly prefer able to optimize the common case of a ZST comparer.

There's a technique for doing this very selectively with transparent wrapper types. If I can't implement a trait for, e.g., u32 then I implement it on WrapU32. Usually of course something much more complex than u32, often when it's been allocated somewhere else by something else and should not be moved from there. Then:

#[repr(transparent)]
#[derive(bytemuck::TransparentWrapper)]
struct WrapU32(u32);

Then in many instantiations the type parameter that would usually be u32 is substituted for WrapU32 and this works out to use the impl that I actually intended, which may differ arbitrarily from the standard one. For instance this works for HashSet where you can impl Borrow<WrapU32> for u32 and then the whole thing can be queried with a u32, yet it does not leak the implementation detail of a changed value type very far.

The upside of this is that it is rigid enough to be clear sound where it works, which I think should also be very instructive when thinking about the potential soundness problems with any such proposal by providing a lower-bound and clear, testable semantics. For instance, converting reference-to-array or reference-to-slice between such types is perfectly fine. The downside is that of course the types actually differ and even though you can shallow cast between u32 and its wrapper, other data structures / type constructors are in general completely out of reach. You might be able to convert a Vec<u32> to a Vec<WrapU32> by-value through into_raw_parts but converting a mutable reference between those targets is already very dangerous ground, UB-wise. Transmuting a HashMap takes guts and confidence in pinning the internals of the library. If the language wanted to provide a form of coercion for wrappers similar to it providing unsize coercion, I think you'd need an explicit opt-in from data structures.

1 Like

The immediate example that I can think of is a BinaryHeap<usize> where the elements are indexes into another collection whose items define the order of the BinaryHeap and you can't consume it to create the BinaryHeap. This also comes up whenever you need to build some sort of auxiliary index over a collection of data.

1 Like

What do you exactly mean by "scoped impls via imports”?

Because while reading that, something like

use other::crate::other::module::impl std::ops::Deref for Utf8Buf;

came to my mind. Combined with your pub impl std::ops::Deref (I'm not sure if pub is the correct keyword here but it doesn't really matter at this point) for marking impls that are not implicitly resolved - I think it could work pretty well. Was it ever discussed?