This is an issue that I’ve been wrestling with for a little while, and I think there’s a core way of expressing our APIs that Rust is currently missing. I need to write this up a little bit more in the “RFC” language, and make the examples a bit more abstract, but I wanted to get other people’s opinions on the concept before doing so. (I’m actually unsure if having the motivation be a specific concrete motivation from a real crate is desirable or not here)
Proposal: Sealed traits
This RFC proposes adding the #[sealed]
annotation to the language. When a trait is marked as #[sealed]
, it becomes illegal to add an impl for the trait outside of the crate where it was defined.
Motivation
Let’s look at a concrete example from diesel
. In 0.4, we have a trait hierarchy that can be simplified to this:
trait FromSql<T> {}
trait FromSqlRow<T> {}
impl<T, ST> FromSqlRow<ST> for T where
T: FromSql<ST>,
{}
impl<A, B, TA, TB> FromSqlRow<(TA, TB)> for (A, B) where
A: FromSql<TA>,
B: FromSql<TB>,
{}
Similar to Into
and From
, implementing FromSql
implies an automatic FromSqlRow
impl. FromSqlRow
is additionally implemented for tuples. However, when we tried to make FromSql
be abstract over a database backend, this structure becomes illegal.
trait FromSql<T, DB> {}
trait FromSqlRow<T, DB> {}
impl<T, ST, DB> FromSqlRow<ST, DB> for T where
T: FromSql<ST, DB>,
{}
impl<A, B, TA, TB, DB> FromSqlRow<(TA, TB), DB> for (A, B) where
A: FromSql<TA, DB>,
B: FromSql<TB, DB>,
{}
The reason this is illegal is that the following is now possible in a child crate:
impl FromSqlRow<(i32, i32), MyLocalType> for (i32, i32)
Without the addition of the DB
parameter, there is no way for a child crate to impl FromSqlRow
in a way that overlaps with the blanked impls that exist, as there would be no crate local type, and tuples are not #[fundamental]
.
However, the intention of this API is that FromSqlRow
is not something that can be implemented directly, and types wanting to implement it should impl FromSql
. Under this proposal, FromSqlRow
can be marked as #[sealed]
, making this contract explicit to the compiler, and allowing blanket implementations that would currently be illegal under the coherence rules.
An additional benefit of this change is that a crate is freely able to add members to any #[sealed]
trait without it being a breaking change, as they can be confident that they control all implementations of that trait.
Drawbacks
By definition, this change means that crates will potentially have traits as part of their public API which cannot be directly implemented. This could be potentially confusing or frustrating for users of that crate. This is easily mitigated by providing a proper error message in this case, which shows all derived implementations of that trait that the user can take advantage of. In the given example, the error message would ideally specifically mention implementing ToSql<ST, DB>
.
Alternatives
Many of the “impossible” cases that this would solve will be made possible by specialization. However, I do not believe that specialization covers all of these cases. Additionally, it does not remove the desire to have a “public but not implementable” trait in a crate’s API, and the ability to freely modify a trait as a non-breaking change is a major concrete benefit that is not covered by specialization.
Thank you for taking the time to review this proposal.