This is a rough idea I’ve had for a while for solving the coherence issues I tend to come across. I’m skeptical of the potential/necessity of this RfC, but I decided to just post it anyway and see what others think.
- Feature Name: forward_impl
- Start Date: 2017-01-18
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
Add the ability to “forward-declare” that an impl exists, to be used to sidestep the orphan rules in multi-crate scenarios.
Motivation
Currently we have a set of “orphan rules” that ensure that you can never come across a situation where two crates provide conflicting impls for the same type-trait pair.
Part of the reason the rules are strict is that they try to ensure the property that your program will not break simply by virtue of including another crate. If this was not the case we could simply allow any impl, do a global search during compilation, and error out when we find conflicts.
This is a good property to have. The programmer should not have to deal with errors that are fundamentally not their problem; errors that originate from other crates.
However, sometimes the programmer is responsible for these “other crates”. Often, crates get split up for better modularity, better compile times, or simply the ability to pull in only a part of the functionality offered by a crate. In these cases, the crates are maintained by the same person and are logically a single unit.
Examples of this pattern include the myriad of stdlib crates shipped with rustc,
or the components/
crates in Servo.
For example, let’s say
we want to implement Add<str> for str
where the output is a String
. This
can’t currently be done, since implementations on str
can only exist in libcore,
but the output type is defined outside it. We’ve had similar issues crop up in Servo,
which have usually been hacked around.
It would be nice to have an escape hatch for punching through the coherence rules in such cases.
Detailed design
Introduce a new item type, “forward impl”. This is basically a forward-declaration, and lets an impl exist split across two crates.
// libcore
forward impl StringAddition<S> {
impl Add<str> for str {
type Output = S;
fn add(&self, other: &Self) -> S
}
}
forward impl StringToOwned<S> {
impl str {
fn to_owned(&self) -> S;
}
}
// libstd
fill impl core::StringAddition<String> {
impl Add<core::str> for core::str {
type Output = String;
fn add(&self, other: &Self) -> String {
// ...
}
}
}
fill impl core::StringToOwned<String> {
impl core::str {
fn to_owned(&self) -> String {
// ...
}
}
}
Here, libcore declares that a pair of impls may exist. Other crates must operate on the
assumption that the impl exists as far as coherence is concerned. However, from the point
of view of typechecking and code generation, this implementation does not exist unless
libstd
has been pulled in explicitly. The generics on the impl represent types
which the downstream crate must fill in.
The fill impls work like lang items, only one crate may define them in the whole dep graph. The intention of the system is not to ever have conflicts here. If I, the owner of crate A, add a forward impl in A, I should fill it in in crate B from the same “package” (e.g. “Servo components”, “Rust std distribution”, etc). Users are allowed to include A but not B, but they are not supposed to fill in the forward impl unless it’s an exceptional circumstance (e.g. implementing your own libstd).
In case one of the unbound types on the forward impl is in the inner impl’s trait or target, from the point of view of coherence from other crates this is to be treated as a blanket impl. So, if we have:
// this is a simplistic example; such a forward impl isn't necessary since
// you can already impl foreign traits on foreign types if you substitute local
// types in the parameters of the foreign type. But in more complex situations
// (e.g. more generics) this would be useful.
// crate A
struct MyStruct<T>;
forward impl SomeImplName<S> {
impl SomeTrait for MyStruct<S> {
...
}
}
// crate B
fill impl SomeImplName<SomeType> {
impl SomeTrait for MyStruct<SomeType> {
...
}
}
from the point of view of crates including A but not B, this acts as if there is an impl<T> SomeTrait for Struct<T>
in A from the POV of coherence. Ordinarily, things like
impl a::SomeTrait<LocalType> for a::MyStruct<LocalType>
are allowed in foreign crates, however this
will make them impossible since there’s no guarantee as to what that filling impl will substitute (and we
assume that the filling impl will substitute any type, treating it as a blanket impl)
From the point of view of crate A, this impl simply doesn’t exist. crate A is free to write
its own impls of SomeTrait for MyStruct<SomeOtherLocalType>
. From the POV of B and crates including
B, this impl does exist, so they too may write more such impls.
How We Teach This
Dum de dum do this later.
Drawbacks
None whatsoever. My rfcs are perfect.
But seriously, this feels a bit like a hack (like #[fundamental]
), and it’s unclear how useful it will be.
It’s another niche/confusing feature, like #[fundamental]
. On top of that, it makes it possible for your crate to
break by the mere existence of another crate in the dep graph. This is already true because of lang items, but
lang items don’t get used so this hasn’t been a problem, whereas I would like to see this feature get used. It
should be fine as long as people use this feature with discipline – not defining fill impls when they
weren’t the ones who defined the forward impls.
Alternatives
Just don’t do it. It’s not that pressing a need. I’ve felt it often, but usually it can be awkwardly worked around.
Unresolved questions
Do we even allow inherent impls to be forward declared? What’s wrong with traits?
What should the syntax be? My original proposal was:
#[forward_declare(StringAddition)]
impl Add<str> for str {
type Target = _;
fn add(&self, other: &str) -> Self::Target;
}
and
#[fills_declaration(core::StringAddition)]
impl Add<str> for str {
type Target = String;
fn add(&self, other: &str) -> String { /* body */}
}
In general the forward impl
proposal is introducing a tricky context-sensitive keyword that I’d rather avoid.
impl forward
might be better. Or impl(forward)
. Idk.
This doesn’t solve the problem when the types involved and the trait being implemented are in three different crates.