I know this proposal seems super weird. But it makes sense!
In fact, in some useful cases, it helps derive From<Z> for X when From<Y> for X and From<Z> for Y are both available.
Let me elaborate with a toy example:
pub struct A<X>(X);
impl<X, Y> From<X> for A<Y> where
Y: From<X>
{
fn from(value: X) -> Y {
A(Y::from(value))
}
}
pub struct B<X>(X);
impl<X, Y> From<X> for B<Y> where
Y: From<X>
{
fn from(value: X) -> Y {
B(Y::from(value))
}
}
We can notice that A::<B<usize>>::from(0usize) works when From<usize> for usize is implemented.
We also notice that when impl From<X> for X { ... } presents, there will be conflict implementations, because we cannot prevent the X = B<Y> case in impl<X, Y> From<X> for B<X> { ... }, which means another From<B<X>> for B<X>.
Don't implement From<X> for X by default doesn't mean we cannot implement it manually.
For most types T in std-lib, I agree that From<T> for T is necessary.
I know what you mean. Every type have let value: X = ...; X::From(value), not only the types defined in std-lib. Can we implement From<X> for X as an auto trait like Sized? For example, make it only applicable to type definitions where no type parameters present.
One significant aspect of all auto traits so far is that they are all marker traits i.e. they have no methods. In fact, AFAICT marker (and thus auto) traits have no associated items at all.
This proposal would change that. I'm curious to see how others would feel about that, assuming it's possible at all.
If an auto trait or marker trait were to have a method or associated item, how would we choose between different implementations of the same trait for a type? You could do it like specialization, but then you get the exact same soundness issues as specialization. Also you can currently cast away auto traits from trait objects. This only works because auto traits can not affect the vtable due to never having any methods.
I'm wondering whether Rust could deliberately allow overlapping implementations/specialization here. The default implementation is so boring that maybe it wouldn't hurt to allow overriding it selectively?
There is an even more fundamental reason why transitive From is impossible:
There may be multiple routes to get from one type to another by transitivity. So a "transitivity" impl conflicts not only with other impls, but also with other instances of itself.
Observe the error that rustc gives when we try to write this, even for a custom trait:
trait From2<T> {
fn from2(value: T) -> Self;
}
impl<A,B,C> From2<A> for C where C: From2<B>, B: From2<A> {
fn from2(value: A) -> C {
From2::from2(From2::from2(value))
}
}
error[E0207]: the type parameter `B` is not constrained by the impl trait, self type, or predicates
--> src/lib.rs:5:8
|
5 | impl<A,B,C> From2<A> for C where C: From2<B>, B: From2<A> {
| ^ unconstrained type parameter
Because B is unconstrained, there could be multiple types B that would fill in this impl, producing conflicting implementations of From2<A> for C.
When the compiler needs to select an impl for Foo<'??>: From<Foo<'??>> while monomorphizing, the new impl will overlap with impl<T> From<T> for T. However selecting impl<T> From<T> for T will be unsound, as it would allow transmuting lifetimes.
So it seems the new impl should be preferred. However consider this other example:
In this example however selecting the new impl would be unsound, since it's valid only when the converted value actually has a 'static lifetime, which cannot be known at monomorphization time.
Ultimately the problem is that impl<T> From<T> for T is not really that "boring" at the type level. It mentions the exact same type T twice, and that's something that cannot be fully checked at monomorphization time (it can only be done modulo lifetimes, but that's enough to cause problems).
The status quo is we consciously decided that lifetime can't affect codegen, and it won't change, thus the result of lifetime specialization problem.
But pure theoretically speaking, I think this is possible in a hypothetical Rust. We just need a sound deterministic way to decide arbitrary lifetime relation, so we can actually choose one implementation for each case at mono time (instead of choose one for all case).
This might introduce some weird behavior since it's not clear what relation we choose. But at least it's sound and deterministic . It just makes reasoning about program behavior much harder. Totally not a major downside!
You mean that it should only work if there is only one path?
This would mean that any library implementing any additional From impl for any type, would be a breaking change. That would be bad.
Also, in cases where there are a lot of From impls, it might be difficult for the compiler to do an exhaustive search to prove that there is no other path, even if that turns out to be true.
That would need specialization, something that is currently unstable because it is unsound in the general case, especially when lifetimes get involved. And from what I can see, that's not likely to change any time soon.
Perhaps. But unfortunately not all cases, and that would be required for a blanket impl.
I don't see why there is a need to search for other path (not sure which part of the compiler you are talking about).
For a pair of specific types X and Y, there either exists a From<X> for Y or it doesn't. And since From is implemented if and only if there is a unique path between X and Y, the subpaths must also be unique.
Clarification: if a person manually implements A -> B -> C and also A -> D -> C, and then tries to use C in a place that needs From<A>, what does the compiler do? Does it say "I saw A -> B -> C first so I'll use that one"? Or "both paths exist, so you can't"? Or is the person somehow prevented from manually implementing both A -> B -> C and A -> D -> C in the first place?
How can this be possible? What will compiler tell you about following code?
impl From<A> for C {
fn from(value: A) -> C {
// make use of From<A> for B
// make use of From<B> for C
}
}
impl From<A> for C {
fn from(value: A) -> C {
// make use of From<A> for D
// make use of From<D> for C
}
}
Sorry, when I said "manually implements A -> B -> C", I meant manually implements both From<A> for B and From<B> for C, and does not manually implement From<A> for C. What happens then?