I did some reading up and more thinking about this proposal:
// You have a local trait:
trait Proxy;
// And you have a blanket impl of a foreign trait,
// but it is bound by the local trait:
impl <T: Proxy> ForeignTrait for T { ... }
// That implementation make the trait special.
// Now, my crate can only implement Proxy for local types
// (as if `Proxy` was defined in a parent crate)
// Child crates can also implement Proxy for their local types
// (in which case the blanket impl will apply too)
I'll refer to this proposed pattern as a proxy trait, which implements foreign proxied traits, and the crate it is in is a proxy crate.
I thought about this proposal mainly in terms of the reasoning used to create the orphan rules. Namely, the reasoning was concerned with balancing the ability of child crates to implement the traits which they need to, and the ability of parents crates to evolve without making breaking changes all the time. See the Background section below for how things evolved. So the main concerns were child crates, parent crates, and what parent traits can and cannot do without breaking child crates.
Becoming a proxy trait is a breaking change
Proxy traits are blanket implementations, and creating a new blanket implementation is a breaking change. This is pretty obvious with a little thought: If I make my existing trait a proxy trait, any child crate that implemented both the proxy trait and the proxied trait themselves will break.
Proxy crates play a new role that constrains child crates
Technically, a proxy trait constrained to local types would not change what implementations can exist. Everything you could do with such a proxy trait, you can do today, though it would involve a lot of repetition. However, the proxy crate is a third type of crate in addition to parents and children, with new abilities, and these abilities impose new restrictions on child crates.
If I am a child crate of a proxy crate, and I implement the proxy trait, I give up the ability to implement the proxied traits. And the proxy crate has the ability to change the implementation of the proxied traits for my local types, even though neither the types or the traits are local to the proxy crate.
You may say "so what?", perhaps thinking of one of the example cases that was discussed:
trait DisplayAsDebug: Debug {}
impl<T: DisplayAsDebug> Display for T { ... }
"Giving up" the ability to implement Display
on my own types would be the entire point of implementing DisplayAsDebug
in my child crate -- I want the proxy crate to take care of things for me. But consider this example from the OP:
// FromBlockCipher is the (local) proxy trait
// FromKey is a foreign trait
impl<T> FromKey for T
where T: FromBlockCipher, T::BlockCipher: FromKey,
In this case, the proxy trait serves a purpose separate from just being a proxy. There may be reasons that I, a child crate, want to implement FromBlockCipher
on my types without giving up my ability to implement (or ability to never implement!) FromKey
. This is a new constraint that child crates may find themselves facing. The effect is similar to that of a negative trait bound.
Additionally, I cannot implement both ProxyTraitA
from crate A
and ProxyTraitB
from crate B
, if the intersection of their proxied traits is not empty. This is a new kind of conflict between "sibling" crates.
There may be an argument for limiting proxy traits to have empty bodies, so that their only purpose is proxying. And perhaps a further argument for not allowing proxy traits in trait bounds (to avoid trait UsefulTrait: ProxyTrait { ... }
). Or perhaps trait implementations are just the wrong approach and instead some sort of macro marker or similar should be considered. (I haven't given these guards against conflicts too much thought yet.)
Relationships between proxy crates and parents
A proxy trait "acts like a foreign trait" in the sense that you can only implement it for local types. Where exactly does this fall in the crate hierarchy? Consider this:
trait Proxy;
impl<T: Proxy> ParentOne for T { ... } // as if in ParentOne's crate
impl<T: Proxy> ParentTwo for T { ... } // as if in ParentTwo's crate
From the perspective of the current rules, it is as if each proxy implementation takes place in the crate of the proxied trait, and as if all of those parent crates had a common ancestor crate where trait Proxy
was defined.
So in some sense, the proxy create is creating some sort of new common ancestor for its parent crates. But only the proxy crate's children can see it. I couldn't come up with a scenario where this breaks things or creates new ways of breaking things, but I'm not terribly confident about that. I'm curious to know what other people think.
What's the color of the boat house at Hereford?
If proxy traits come to be, I feel they should have their own distinct syntax or a keyword to call out their proxy-ness. I feel their special behaviour would be too surprising without it; you can't look at a blanket implementation and tell it's a proxy implementation without knowing the locality of the traits in bounds and the trait being implemented. Even if you know those things, you would have to think of it.
Background on the current orphan rules
-
RFC 2451 lays out the orphan rules in detail as they are today.
- It's an expansion of RFC 1023, which changed the rules in order to give parent crates the ability to evolve without breaking child crates.
-
RFC 1105 details what is and isn't a major breaking change.
- RFC 2451 also corrects RFC 1105 by pointing out that a blanket implementation is a breaking change. In non-exact surface terms, a blanket impl is an impl for
T
(but not LocalType<T>
). The actual rules are more involved; see RFC 2451.
- Be careful as this correction has not been made applied to RFC 1105's text.
Thanks for reading
I know this was a long one.
Incidentally, @newpavlov -- are we still on topic?