Continuing the discussion from Limited Opt Out of Orphan Rule:
What I've been thinking about recently (which is kinda related) is named impls with anonymous or explicit impl generics.
This would be a very big change, so it is super unlikely to happen, but I thought I'd share it anyway, perhaps it's food for thought.
The idea is the following: in addition to normal impls that would continue to be subject to the orphan rules, one could create named impls.
(I'm going to use $
for the syntax here, but it's up for bikeshedding)
impl<T> $name MyTrait<T> for T {}
Named impls could be created by any crate for any type/trait combination. Named impls could be imported/exported like traits, structs, enums, etc. Importantly, named impls are allowed to overlap with a normal impl or a different named impl, so this would be allowed:
impl<T> MyTrait for T {}
impl<T> $my_impl_1 MyTrait for T {}
impl $my_impl_2 MyTrait for MyStruct {}
Impl selection would happen wherever a concrete type is substituted for a type parameter. Impl selection would have an explicit syntax similar to fully qualified syntax, and some mechanism of eliding impl selection in an unambiguous way (just like lifetime elision is perfectly unambiguous from the syntax alone).
Then there would be the impl generics:
fn sort<$ordering: Ord, T: $ordering>(slice: &mut [T])
...which one could use like this:
impl $my_ordering Ord for i32 { ... }
let mut a = [1, 5, 8, 4, 2];
sort::<$my_ordering>(&mut a);
//or
sort((&mut a) as &mut [impl $my_ordering]) // this is ugly, we would need some better syntax
The "normal" trait bounds would act like anonymous impl generics. For backwards compatibility, different trait bounds on the same item would get the same impl generic.
fn foo<T: Add, U: Add>() {}
// would be roughly the same as
fn foo<$o: Add, T: $o, U: $o>() {}
I would actually want them to become separate impl generics, like this:
fn foo<T: Add, U: Add>() {}
// would be roughly the same as
fn foo<$o1: Add, $o2: Add, T: $o1, U: $o2>() {}
...but this would violate the expectation that T::Output == U::Output
if T == U
. I'm not sure how much this would be a problem in practice, but probably there is some case where it matters.
In a way, this feature could be used as a poor man's specialization. If I understand correctly, the problem holding back specialization is that the trait solver runs before the borrow checker analyses lifetimes, and the borrow checker is able to decide whether a specific system of lifetime constraints is satisfiable or not, but having the result of that feed back to impl selection is currently not possible.
My proposal would require users to specify unambiguously which impl they want to use, and then the borrow checker could verify whether that choice works.
For example, the
impl From<T> for T {}
impl in the standard library could become named, which would allow crates to blanket impl From<LocalType>
colliding with it. And then the user would have to specify at the call site which one they meant.