Named impl with Implementation Selection Variant

Rust named impl Draft

The purpose is clear: to bypass Rust's orphan rule.
The proposed syntax should be more fluent to write, more natural to read, and more complete than the referenced proposals.

Advantages Over Other Drafts

  1. By forbidding named implementations for specific traits, the hashtable problem is cleanly bypassed.
  2. Allow overlapping of named trait implementations, resolving the implementation during monomorphization, thus enabling specialization and partial specialization.
  3. The approach is more ergonomic, more explicit, and more complete.

New Grammar

  1. Introduce named trait implementations (named-impl)
pub impl<T> Trait1 for Struct1<T> use ST1 {
    fn trait1_fn(&self) {
        println!("named impl ST1");
    }
}
  1. Introduce a new type definition syntax, the Implementation Selection Variant (Impl Variant for short)
type Struct1Default = Struct1<i32> + _ use default;
type Struct1WithST1 = Struct1<i32> + Trait1 use ST1;

Overview

// Default impl for struct and trait, same as Rust 2024 edition
pub struct Struct1<T>(T);
pub trait Trait1 {
    fn trait1_fn(&self);
}
impl<T> Trait1 for Struct1<T> {
    fn trait1_fn(&self) {
        println!("default impl");
    }
}

// New named-impl definition, can be defined in any crate
// ST1 below lives in the Type Namespace
pub impl<T> Trait1 for Struct1<T> use ST1 {
    fn trait1_fn(&self) {
        println!("named impl ST1");
    }
}

// Forbidden: using `default` as a named-impl name
pub impl Clone for i32 use default {} // ❌

// Import with `use` just like structs and traits; the full path mod1::ST1 can also be used
use mod1::{Struct1, Trait1, ST1};

// Type definitions
type Struct1Generic = Struct1<i32>;
// A generic type, which by default resolves to the following line: Struct1<i32> + _ use default

type Struct1Default = Struct1<i32> + _ use default;
// A concrete type, behaving exactly like Struct1 in Rust 2024; no named-impl is used.
// `default` is a weak keyword here.

type Struct1WithST1 = Struct1<i32> + Trait1 use ST1;
// A concrete type, same layout as Struct1, but uses ST1's implementation for Trait1.
// The trailing `_ use default` is implicitly omitted.

// Different impl => different types
assert!(TypeId::of::<Struct1Default>() != TypeId::of::<Struct1WithST1>());

// Complex definition 1
type ComplexType1 = &(i32 + Trait1 use ST1) + Trait2 use ST2;

// Complex definition 2: overlapping implementations and priority rules
type ComplexType2 = Struct1<i64>
    + From<i32> use mod2::Struct1Fromi32
    + From<_: Add> use default
    + From<_> use StructFrom
    + Trait1 use ST1;
// Multiple named-impl generic parameters; overlapping of traits is allowed.
// When acting as From<i32>, Struct1Fromi32 is used; as From<&str>, StructFrom is used.
// The first matching trait (before `use`) from left to right wins and its implementation is used.


// Type conversions
let s = Struct1(1); // defaults to Struct1<i32> + _ use default
let mut s_ref_named: &(Struct1<i32> + Trait1 use ST1) = &s; // βœ… type conversion
let s_ref_default = s_ref_named as &(Struct1<i32> + _ use default); // βœ… mutual conversion
s_ref_named = s_ref_default; // ❌ implicit conversion without explicit type annotation is forbidden

let s4 = Struct1(4); // defaults to Struct1<i32> + _ use default
let s5: Struct1<i32> + Trait1 use ST1 = s4; // move plus type conversion

let vs: Vec<Struct1<i32>> = vec![]; // defaults to Vec<Struct1<i32> + _ use default>
let vs1: &Vec<Struct1<i32> + Trait1 use ST1> = &vs; // Struct1 with impl variant can appear anywhere in the type
let vs2: &Vec<Struct1<i32> + _ use default> = vs1;


// End usage
// Any T: Trait1 can be instantiated with both Struct1<i32> + _ use default and Struct1<i32> + Trait1 use ST1

// 1. Direct call
s5.trait1_fn();

// 2. UFCS
<Struct1<i32> as Trait1 use ST1>::trait1_fn(&s5);

// 3. Generic function
fn use_trait1<T: Trait1>(a: &T) {
    a.trait1_fn();
}

use_trait1(s_ref_default); // default impl
use_trait1(s_ref_named); // named impl ST1

// 4. Generic type
pub struct Struct2<'a, T: Trait1>(&'a T);
impl Struct2<'a, T: Trait1> {
    pub fn use_inner(&self) {
        self.0.trait1_fn();
    }
}

let s21 = Struct2(s_ref_default);
s21.use_inner(); //  default impl

let s22 = Struct2(s_ref_named);
s22.use_inner(); // named impl ST1


// trait object
let mut dyn_trait1: &dyn Trait1;

dyn_trait1 = s_ref_named;
dyn_trait1.use_inner(); // named impl ST1

dyn_trait1 = s_ref_default;
dyn_trait1.use_inner(); //  default impl


// impl trait
// 1. `impl trait` in argument position is merely syntactic sugar for generics
fn use_trait1(a: &impl Trait1) {
    a.trait1_fn();
}
use_trait1(s_ref_default); // default impl
use_trait1(s_ref_named); // named impl ST1

// 2. `impl trait` in return position
fn return_trait1() -> impl Trait1 {
    let s4 = Struct1(4); // defaults to Struct1<i32> + _ use default
    return s4 as (Struct1<i32> + Trait1 use ST1); // move plus type conversion
}

Prohibition Rules

  1. Named-impl must be forbidden for Copy, Drop, Send, Sync, Unpin, etc.
#[disable_named_impl(all)]
pub trait Copy {}

#[disable_named_impl(all)] can be used by external code and placed on traits, structs, enums, etc.

  1. Named-impl must be forbidden for Hash, Ord, PartialOrd, Eq, PartialEq, etc. at least when a default implementation exists.
#[disable_named_impl(exist_default)]
pub trait Hash {}

#[disable_named_impl(exist_default)] can be used by external code and placed on traits, structs, enums, etc.

Extended Usages

  1. Implementing external traits for external types, bypassing the orphan rule.

  2. Compile-time proxy, or function decorator
    (akin to java.lang.reflect.Proxy or Python decorators)

impl fmt::Display for i32 use ProxyDisplay {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "before ")?;
        (self as &(i32 + Display use default)).fmt(f)?;
        write!(f, " after")?;
        Ok(())
    }
}

// Generic proxy, but might be impossible to write due to recursion
impl<T: Display> fmt::Display for T use ProxyDisplayDefault {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "before ")?;
        (self as &(T + Display use default)).fmt(f)?; // ❓
        write!(f, " after")?;
        Ok(())
    }
}
  1. Specialization, partial specialization
// Overlapping implementations and priority rules
type ComplexType2 = Struct1<i64>
    + From<i32> use mod2::Struct1Fromi32
    + From<_: Add> use default
    + From<_> use StructFrom
    + Trait1 use ST1;
// Multiple named-impl generic parameters; overlapping of traits is allowed.
// When acting as From<i32>, Struct1Fromi32 is used; as From<&str>, StructFrom is used.
// The first matching trait (before `use`) from left to right wins.

Implicit Type Generics, Optional

  1. Generic bounds can be structs or enums
#[derive(Default)]
struct Struct1;
impl Trait1 for Struct1 {};
impl Trait1 for Struct1 use TS1 {};
fn use_struct1<T: Struct1>() {
    T::default().trait1_fn();
}

use_struct1<Struct1 + _ use default>();
use_struct1<Struct1 + Trait1 use TS1>();
  1. Implicit type generics: function parameters and return types become generic
  • Advantage: old-style functions can automatically work with named-impl types.
  • Disadvantage: unsound, breaks the function's original intent.
fn use_struct1(s: &Struct1) {
    s.trait1_fn();
}
// Automatically translates to
fn use_struct1<T: Struct1>(s: &T) {
    s.trait1_fn();
}
  • Problem 1: Multiple Struct1s would be forced to the same type.
  • Problem 2: If a parameter is i32 and becomes generic, it destroys logic inside the function that relies on that i32 (e.g., Add or PartialEq).

Simplified Syntax

  1. Named-impl selection parameter simplification
type Struct1Default = Struct1<i32> + use default;
type Struct1WithST1 = Struct1<i32> + use ST1;
type ComplexType = Struct1<i64> + use Struct1Fromi32<i32> + use StructFrom<_> + use ST1;
// The trait type can be inferred; generic parameters follow the impl name.

Alternative Syntax

  1. Use as instead of use
pub impl<T> Trait1 for Struct1<T> as ST1 {
    fn trait1_fn(&self) {
        println!("named impl ST1");
    }
}
type Struct1Default = Struct1<i32> + _ as default;
type Struct1WithST1 = Struct1<i32> + Trait1 as ST1;

<Struct1<i32> as Trait1 as ST1>::trait1_fn(&s5);
  1. Named-impl selection parameters inside <> after generic parameters
type i32AlterDisplay = i32<i32, Display use AlterDisplay>;
type Struct1Default = Struct1<i32, _ use default>;
type Struct1WithST1 = Struct1<i32, Trait1 use ST1>;
type ComplexType = Struct1<i64, From<i32> use Struct1Fromi32, From<_> use StructFrom, Trait1 use ST1>;

//❓ How to express this with the alternative syntax?
type ComplexType1 = &(i32 + Trait1 as ST1) + Trait2 as ST2;

Trait Inheritance Issues

  1. Type bloat.
  2. Whether a subtrait's named-impl can restrict which implementation of the supertrait is used.
  3. More complex upcasting.
trait T1 {}
trait T2 : T1 {}

struct S;
impl T1 for S {}
impl T2 for S {}

impl T1 for S use T1S3 {}
impl T2 for S use T2S3 where S: T1 use T1S3 {} //❓ Optional, forces T2S3 to only be used with T1S3

// Bad news: type bloat
// Good news: the four types are not automatically generated; they don't exist unless written (compile-time and runtime)
let s1: S + _ use default = S;
let s2: S + T1 use T1S3 = S;
let s3: S + T2 use T2S3 = S; // If restricted, it is automatically inferred as the next
let s4: S + T1 use T1S3 + T2 use T2S3 = S;

type Invalid = S + T1 use default + T2 use T2S3; //❌ If the restrictive syntax above is present

Compatibility Breakage

  1. FFI boundaries.
  2. dylib exported functions.
  3. All code that assumes trait object data pointers are the same to judge identity will break.
fn is_same(a: &dyn T1, b: &dyn T1) -> bool {
	let (ptr_a, vt_a) = unsafe { transmute::<_, (usize, usize)>(a) }
	let (ptr_b, vt_b) = unsafe { transmute::<_, (usize, usize)>(b) }
	return ptr_a == ptr_b;
}

Negative Implementations (negative-impls)

  1. negative-impls cannot be used for named-impl
pub impl !Trait1 for i32 use NegTrait1 {} // ❌
  1. Overlap checking between named-impl and negative-impls is deferred to type monomorphization time
#![feature(negative_impls)]
trait DerefMut { }
impl<T: ?Sized> !DerefMut for &T { }

// Overlap with other implementations (including !Trait) is NOT checked at the definition of named-impl
impl DerefMut for &i32 use DerefMutForI32 {} // βœ…

// The overlap check between named-impl and negative-impls happens at type instantiation
type X = &i32 + DerefMut use DerefMutForI32; // ❌

GAT

Orthogonality with GAT

type A1 = S + Iterator use SI64;  // βœ…
type A2 = S + Iterator<Item=i64> use SI64;  // ❌
struct S;

impl Iterator for S {
    type Item = i32;
    fn next(&mut self) -> Option<Self::Item> { todo!() }
}

impl Iterator for S use SI64{
    type Item = i64;
    fn next(&mut self) -> Option<Self::Item> { todo!() }
}

fn use_iter<T: Iterator>() -> T::Item { todo!() }
fn use_iter_i32<T: Iterator<Item=i32>>() -> T::Item { todo!() }
fn use_iter_i64<T: Iterator<Item=i64>>() -> T::Item { todo!() }

use_iter::<S>();      // βœ…
use_iter_i32::<S>();  // βœ…
use_iter_i64::<S>();  // ❌

use_iter::<S + Iterator use SI64>();     // βœ…
use_iter_i32::<S + Iterator use SI64>(); // ❌
use_iter_i64::<S + Iterator use SI64>(); // βœ…

References

As you linked, there's been a bunch of discussions in this area before.

What's the summary of the difference that makes this one fix the issues from the previous ones?

3 Likes

Objectionable. I might want to sort tuples (u32, u32) by their second argument, or use another custom comparator, while there is a default PartialOrd implementation.

Let me explain the Prohibition Rules

  1. Named-impl must be forbidden for Copy, Drop, Send, Sync, Unpin, etc
let a = Box::new(42);
let b = a as Box<i32> + Copy use BadCopy;
let c = b; // copy, not move
drop(b);
drop(c); // double free
  1. Named-impl must be forbidden for Hash, Ord, PartialOrd, Eq, PartialEq, etc. at least when a default implementation exists.
struct SortedList<T: PartialOrd> {} // always keeps elements in order defined by T: PartialOrd 
let sl_org = SortedList::new((1,2),(2,3),(3,1)); // invariant: (1,2), (2,3), (3,1)
let sl_alt = SortedList::<(i32,i32) + PartialOrd use SortBy1>::new((1,2),(2,3),(3,1)); // uses different ordering, yields (3,1), (1,2), (2,3)
// Dangerous: converting an existing collection to a variant with a different comparator
let sl_bad = s1 as SortedList<(i32,i32) + PartialOrd use SortBy1>; // breaks the internal invariant

And so, custom comparator also breaks BTreeMap, custom Hash will break HashMap.

Your last example could just forbid the cast from SortedList<(i32, i32)> to SortedList<(i32, i32) + PartialOrd use SortBy1> and not the named implementation SortBy1 itself. After all, PartialOrd and PartialOrd use SortBy1 are different traits in a single namespace, which happen to share one interface.

That makes me think that for the first half no compiler changes are needed at all: just make your traits generic. Using it ergonomically (i.e. type aliases) is another problem, though, and should depend on current codebase patterns.

pub enum StandardDef {}
pub enum RevStandardDef {}

pub trait PartialOrd<Rhs=Self, OrderingKind=StandardDef> {
    fn partial_cmp(self, rhs: Rhs) -> Option<Ordering>;
}

impl<Lhs, Rhs> PartialOrd<Lhs, RevStandardDef> for Rhs
where
    Lhs: PartialOrd<Rhs, StandardDef>
{
    fn partial_cmp(self, other: Lhs) -> Option<Ordering> {
        <Lhs as PartialOrd<Rhs, StandardDef>>::partial_cmp(other, self)
    }
}

// Or another implementation option doing approximately the same:
/*
impl<Lhs, Rhs> PartialOrd<Rhs, RevStandardDef> for Lhs
where
    Lhs: PartialOrd<Rhs, StandardDef>
{
    fn partial_cmp(self, rhs: Rhs) -> Option<Ordering> {
        <Lhs as PartialOrd<Rhs, StandardDef>>::partial_cmp(self, other)
            .map(|order| order.flip())
    }
}
*/

Your last example could just forbid the cast from SortedList<(i32, i32)> to SortedList<(i32, i32) + PartialOrd use SortBy1>

No, casting is the core part. And it's almost impossible to disable special cast.

And you only need pub fn slice::sort_by<F>(&mut self, compare: F).

Advantages Over Other Drafts

  1. By forbidding named implementations for specific traits, the hashtable problem is cleanly bypassed.
  2. Allow overlapping of named trait implementations, resolving the implementation during monomorphization, thus enabling specialization and partial specialization.
  3. The approach is more ergonomic, more explicit, and more complete.

This breaks adding or broadening a trait impl being SemVer-safe.

The "hashtable problem" is a stand-in for the general category of traits with consistency requirements. This includes third-party definitions like the (de)serialisation traits in Serde or the reflection traits in facet (in combination with type keying).

It's not quite a soundness problem, but the logical consistency aspect here is important enough that, in my opinion, no proposal that special-cases specific traits can solve it ergonomically.

Actually, It's not SemVer-safe right now.

  • lib A-1.0: struct A
  • bin B:
trait T {
	fn fn1();
}
impl T for A {
	fn fn1() {}
}
A::fn1();
  • lib A-1.1, add a new trait and impl, it breaks bin B
struct A;
trait T1 {
	fn fn1();
}
impl T1 for A {
	fn fn1() {}
}

No, it doesn't because A::fn1(); in B won't try to resolve to A's implementation unless B explicitly imports T1.


Edit: The case that would break it here (or potentially silently change behaviour!) is if A added an associated function fn1 on A directly.
This is usually considered acceptable in that the unqualified call in B is considered fragile.

It's better practice to qualify it as ::<A as T>::fn1() there for such static calls. That's the reason why I think of "extension traits" that are intended to be called into statically without qualification as a bit sketchy sometimes.