The change in 1.83
Consider this example:
pub trait Trait
where
// Has always been implied
Self::Assoc: Iterator,
// Used to not be implied, but is now implied
<Self::Assoc as Iterator>::Item: Clone,
{
type Assoc;
}
The below was a non-breaking change pre-1.83, but is now a breaking change:
where
Self::Assoc: Iterator,
- <Self::Assoc as Iterator>::Item: Clone,
It used to be non-breaking because downstream had to repeat the bound. Now that it's implied, downstream does not have to repeat the bound, and can implicitly rely on it.
This is the PR. It was not announced nor mentioned in the release notes.
Bounds on the associated types of supertraits
The change seems to be pretty narrowly focused, but I still find it concerning. Let me set the stage for why by talking about bounds on the associated types of supertraits. Note: as far as my testing goes, these were not affected by the change in 1.83 (but I'm not completely confident about what all was affected).
Consider this arrangement:
pub trait Trait {
type Assoc;
}
pub trait SubTrait: Trait
where
// Not implied (consumers must repeat the bound)
Self::Assoc: Clone,
{}
Today, the SubTrait
maintainer can make either of these non-breaking changes:
// Remove the bound (commit to never having it back)
pub trait SubTrait: Trait {}
// Make the bound implied (commit to never removing it) [MSRV: 1.79]
pub trait SubTrait: Trait<Assoc: Clone> {}
That is, they have three choices regarding the Assoc: Clone
bound.
- Don't have the bound (breaking change to add it)
- Have the bound but not implied (non-breaking to drop it or to make it implied)
- Have the bound in implied form (breaking change to remove it or make it non-implied)
(1) and (3) are commitments, but you can go from (2) to either (1) or (3).[1] (3) has only been possible since 1.79, so you could consider (2) to be the default position; all stable pre-1.79 code with supertrait associated type bounds is in the uncommitted (2) camp.
The ability to go from (2) to (1) in a non-breaking way has been present since Rust 1.0.
Why the change is concerning
If bounds such as the one in the first SubTrait
snippet were to become implied, Rust would be committing everyone in camp (2) to keeping the bound without them having a choice in the matter. Effectively camp (2) is deleted and everyone in it is moved to camp (3). There is no longer a code option which is not a commitment.[2]
Moreover, I consider the current situation with associated type bounds on supertraits to be a kind of test run of additional implied bounds elsewhere. And... I am pleased with how it has turned out! So far that is: I'm pleased that maintainers have the choice between (1), (3), or "by default" (2) -- "I'll decide later".
Making the removal of bounds a breaking change is a major downside of implied bounds generally. Maintainers should have to opt in to such a commitment (or over an edition, have the ability to opt out of the commitment). Or in other words, maintainers should have the choice to not hard commit to the bounds.
That is why the change in 1.83 concerns me (especially given how "silently" the change was made). It implies that the choice may be taken away from maintainers, committing them to their current bounds. Both with the associated type supertrait bounds we have today, and with whatever new implied bounds may come in the future.
I'm making this post to draw attention to those future possibilities (in addition to the 1.83 change).