# 1.83 makes removing certain trait bounds a breaking change

**URL:** <https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089>\
**Category:** language design\
**Created:** [December 30, 2024, 1:00am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089 "2024-12-30T01:00:14Z")\
**Posts on this page:** 15\
**Page:** 1

<div class="post-metadata">

**Author:** ![quinedot](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/quinedot/32/7294_2.png) [@quinedot](https://internals.rust-lang.org/u/quinedot)\
**Post date:** [December 30, 2024, 1:00am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/1 "2024-12-30T01:00:14Z")

</div>

# The change in 1.83

Consider this example:

```rust
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:

```diff
 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.](https://github.com/rust-lang/rust/pull/120752) It was not announced nor mentioned in the [release notes.](https://doc.rust-lang.org/stable/releases.html#version-1830-2024-11-28)

# 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:

```rust
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:

```rust
// Remove the bound (commit to never having it back)
pub trait SubTrait: Trait {}

```

```rust
// 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.

1. Don't have the bound (breaking change to add it)
2. Have the bound but not implied (_non_-breaking to drop it or to make it implied)
3. 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.](https://rust-lang.github.io/rfcs/2089-implied-bounds.html) 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).

* * *

1. MSRV permitting 

2. And if you want to commit to not having the bound, you have to do it before the change in implied bounds lands.

---

<div class="post-metadata">

**Author:** ![jrose](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jrose/32/9591_2.png) [@jrose](https://internals.rust-lang.org/u/jrose)\
**Post date:** [December 30, 2024, 1:50am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/2 "2024-12-30T01:50:11Z")

</div>

~~I think this was always breaking.~~ Consider:

```rust
fn clone_first<T: Trait>(iter: T::Assoc) -> Option<T::Assoc::Item> {
  Some(iter.first()?.clone())
}

```

Nope, I’m wrong, this one also required you to explicitly guarantee Clone in the signature of `clone_first`.

---

<div class="post-metadata">

**Author:** ![fmease](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/fmease/32/12726_2.png) [@fmease](https://internals.rust-lang.org/u/fmease)\
**Post date:** [December 30, 2024, 2:05am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/3 "2024-12-30T02:05:46Z")

</div>

~~Well, it will once/if my PR [Implement consecutive shorthand projections (associated type paths) like `T::AssocA::AssocB` by fmease · Pull Request #126651 · rust-lang/rust · GitHub](https://github.com/rust-lang/rust/pull/126651) lands (needs polish and FCP).~~ Not really relevant actually.

---

<div class="post-metadata">

**Author:** ![fmease](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/fmease/32/12726_2.png) [@fmease](https://internals.rust-lang.org/u/fmease)\
**Post date:** [December 30, 2024, 2:16am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/4 "2024-12-30T02:16:34Z")

</div>

> **Please ignore**
>
> > [@quinedot](#):
> >
> > Today, the `SubTrait` maintainer can make either of these non-breaking changes:
> > 
> > ```rust
> > // Remove the bound (commit to never having it back)
> > pub trait SubTrait: Trait {}
> > 
> > ```
> 
> I don't quite understand? Does that comment suggest it's acceptable to remove any supertrait bounds? If so, that's simply not true. I'm sure I'm missing something.
> 
> ```rust
> // upstream
> pub trait Trait: SuperTrait { } // <-- can't remove `SuperTrait` backward compatibly
> pub trait SuperTrait { type Assoc; } // or any kind of associated item
> 
> // downstream
> fn f<T: Trait>() { let _: T::Assoc; } // or
> fn g<T: Trait<Assoc:>>() {} // or …
> 
> ```

Edit: Ah, I missed that it was part of the other example.

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [December 30, 2024, 5:07am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/5 "2024-12-30T05:07:39Z")

</div>

IMO, it would be reasonable if

```rust
pub trait Trait
where
    Self::Assoc: Iterator,
    <Self::Assoc as Iterator>::Item: Clone,
{
    type Assoc;
}

```

was changed back to its original behavior; but

```rust
pub trait Trait
where
    Self::Assoc: Iterator<Item: Clone>,
{
    type Assoc;
}

```

was to keep the new implying behavior (even though between 1.79-1.82 it hasn't been that way), because it's very unlikely that someone wrote a trait in that time - _using_ the new `Iterator<Item: Clone>`-syntax - whilst deliberately trying to avoid the additional bound to be implied;  
and also, it would help **keep some of the consistency** that's being restored, like

```rust
pub trait Trait {
    type Assoc: Iterator<Item: Clone>;
}

```

works that way since 1.79, and before that, it used to be the case that

```rust
pub trait Trait {
    type Assoc: …something…;
}

```

and

```rust
pub trait Trait
where
    Self::Assoc: …something…,
{
    type Assoc;
}

```

were always equivalent.

* * *

The distinction of

```rust
pub trait Trait
where
    Self::Assoc: Iterator,
    <Self::Assoc as Iterator>::Item: Clone,
{
    type Assoc;
}

```

vs

```rust
pub trait Trait
where
    Self::Assoc: Iterator<Item: Clone>,
{
    type Assoc;
}

```

would also be consistent with the distinction of

```rust
pub trait Trait
where
    Self: Iterator,
    <Self as Iterator>::Item: Clone,
{
}

```

vs

```rust
pub trait Trait
where
    Self: Iterator<Item: Clone>,
{
}

```

---

<div class="post-metadata">

**Author:** ![zirconium-n](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/zirconium-n/32/9078_2.png) [@zirconium-n](https://internals.rust-lang.org/u/zirconium-n)\
**Post date:** [December 30, 2024, 6:49am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/6 "2024-12-30T06:49:01Z")

</div>

> [@steffahn](#):
>
> would also be consistent with the distinction of
> 
> ```rust
> pub trait Trait
> where
> Self: Iterator,
> <Self as Iterator>::Item: Clone,
> {
> }
> 
> ```
> 
> vs
> 
> ```rust
> pub trait Trait
> where
> Self: Iterator<Item: Clone>,
> {
> }
> 
> ```

I have to say this give me "being consistently inconsistent" vibe. I would really expect these two are exactly equivalent. It seems really hard to teach ☹.

To OP: I think the best case scenario is that: we don't need to spell out the additional where bounds, but we can't rely on it either (other than proving `T: Trait` itself). This can be extended to all implied bounds.

---

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [December 30, 2024, 9:39am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/7 "2024-12-30T09:39:54Z")

</div>

> especially given how "silently" the change was made

As far as I understand from PR thread, no one knew about this potential breaking. Everyone agreed, it went through fcp with everyone involved believing that this is positive change without downsides like this. So I don't see any problem with "silence", as there were nothing to communicate.

* * *

Implied bounds feature has a lot of upsides, some amount of sacrifice is acceptable imho. It depends on how many are there examples of maintainers actually using that opportunity to remove the bound.

Additionally, maybe some sort of mechanism to opt out of implicit behavior would help? For example, putting `#[not_imply_bounds]` on top of the trait declaration.

---

<div class="post-metadata">

**Author:** ![Noratrieb](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/noratrieb/32/12440_2.png) [@Noratrieb](https://internals.rust-lang.org/u/Noratrieb)\
**Post date:** [December 30, 2024, 10:58am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/8 "2024-12-30T10:58:01Z")

</div>

In my opinion, `<Self::Assoc as Iterator>::Item: Clone` is the same in spirit as `Self::Assoc: Iterator`, so I think it makes sense that it's treated the same.

I do agree that it should have been in the release notes.

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [December 30, 2024, 1:01pm UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/9 "2024-12-30T13:01:14Z")

</div>

> [@zirconium-n](#):
>
> I have to say this give me "being consistently inconsistent" vibe. I would really expect these two are exactly equivalent. It seems really hard to teach ☹.

Well, the rationale is very simple: `Trait<Item: Bound>` syntax is _new_, so it gets to have the _new_ implied-bounds meaning.

Perhaps just like `-> impl Trait` in traits was _new_, so it got to have the _new_ lifetime-capturing meaning. In other words: a different implicit default meaning _is reasonable_, but should only be introduced over an edition.

* * *

> [@Ddystopia](#):
>
> Additionally, maybe some sort of mechanism to opt out of implicit behavior would help? For example, putting `#[not_imply_bounds]` on top of the trait declaration.

Yes, but only in a _new_ edition. Opt-out then can be a migration strategy for traits that want to keep the old behavior.

Because _enabling_ impliedness on a given trait non-breaking change. Disabling impliedness is a breaking change. Without edition change, or forcing breaking changes upon users (or creating breaking changes in _existing_ version sets that used to be properly semver-compliant), an opt-in the the proper option, not an opt-out.

> [@Ddystopia](#):
>
> Implied bounds feature has a lot of upsides, some amount of sacrifice is acceptable imho. It depends on how many are there examples of maintainers actually using that opportunity to remove the bound.

It's common to use this opportunity in the _general_ context of “implied bounds everywhere”, for structs. In particular, users that might start out with some

```rust
struct Foo<T: Bar> { … }

```

might later realize it’s a good idea to move this `T: Bar` to only the relevant methods of `Foo` that need the `T: Bar`. This is okay, and not a breaking change. Making `Bar` always implied is bad, because then it would be a breaking change, breaking users like

```rust
fn foo<T>(_: Foo<T>) { <T as Bar>::some_method() }

```

* * *

To be fair, I'm _not_ sure whether or not this is actually realistically a thing that crates would have been doing for traits (rather than for structs). But there's still some concerns:

- automatically implied bounds everywhere _only for traits_ might be a reasonable end-goal, but that would be a deliberate design decision, and making `where` clauses for structs and traits behave differently
- whether or not there are instances of such changes (lifting `where` restrictions on traits) being made – with the understanding that this isn't breaking – is a question one can't answer by just thinking about it. And also it's not a question that a normal crater run will answer, either (AFAIK). We _would_ need some sort of analysis that runs on semver-compatible version pairs.

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [December 30, 2024, 1:51pm UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/10 "2024-12-30T13:51:27Z")

</div>

I just noticed that these _also_ work through simplifying other associated-type projections, unlike any other of the "implied/uplifted" bounds to compare to. In code:

```rust
trait Trait
where
    Self: Other, // implied
{
    type Assoc;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1b2bb24907a431ae4729fdb9c2322b49))

```rust
trait Indirectly {
    type This: ?Sized;
}
impl<T: ?Sized> Indirectly for T {
    type This = T;
}

```

```rust
trait Trait
where
    <Self as Indirectly>::This: Other, // not implied
{
    type Assoc;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c272082ab8f028c248093a87508e9904))

```rust
trait Trait
where
    Self::Assoc: Other, // implied
{
    type Assoc;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8e43b7b5c07bca339fddce1e5eb90e98))

```rust
trait Trait
where
    <Self::Assoc as Indirectly>::This: Other, // not implied
{
    type Assoc;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f71f3dd921fcafe5e0c9faa2e967a1ad))

```rust
trait Intermediate {
    type Deep;
}

```

```rust
trait Trait
where
    <Self::Assoc as Intermediate>::Deep: Other, // implied (change in 1.83!)
{
    type Assoc: Intermediate;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=10c56f3a552cbe4c4c2fa71bc36a6bc4))

```rust
trait Trait
where
    // still implied!! (change in 1.83!)
    <<Self::Assoc as Intermediate>::Deep as Indirectly>::This: Other,
{
    type Assoc: Intermediate;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=411c1139e04ae1b42b7a5554c2f34187))

```rust
trait Trait
where
    // still implied!! (change in 1.83!)
    <<Self::Assoc as Indirectly>::This as Intermediate>::Deep: Other,
{
    type Assoc: Intermediate;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d8e1472fda7ae69dc4c82ed65d211b39))

```rust
trait Trait
where
    // still implied!! (change in 1.83!)
    <<<Self::Assoc as Indirectly>::This as Intermediate>::Deep as Indirectly>::This: Other,
{
    type Assoc: Intermediate;
}

```

([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=795544fec7cb241af26ceb130b79093d))

* * *

In the context of @quinedot's original example, this means that (perhaps surprisingly) there's inconsistency between

```rust
pub trait Trait
where
    // Used to not be implied, but is now implied
    <Self::Assoc as IntoIterator>::Item: Clone,
    // Is not implied
    <Self::Assoc as IntoIterator>::IntoIter: Clone,
{
    type Assoc: Iterator;
}

```

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [December 30, 2024, 6:23pm UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/11 "2024-12-30T18:23:47Z")

</div>

> [@steffahn](#):
>
> To be fair, I'm _not_ sure whether or not this is actually realistically a thing that crates would have been doing for traits (rather than for structs).

There really are interesting tradeoffs here, as to what bounds "should" be implied. Gating new implications on edition boundaries and/or choice of syntax is a good conservative approach, but it means users need to hit the extra roadbump of needing to (by straightforward machine applicable compiler error) add the extra bound depending on when the library was published.

Traits are very much a contract between implementer and consumer, so I'd err on the side of thinking that traits very rarely have unimplied bounds that could be removed; if they didn't make semantic sense, why would the author include the bounds, when use sites still have to repeat the bound if they want to use it anyway?

Whereas struct generics can get some "unnecessary" bounds much more easily. Either improperly added as a bound for `derive`d impls, or for functionality used in `Drop`. But otoh, if a trait bound is structurally required (e.g. used for a `pub` type projection), then implying it makes sense, since the bound cannot be removed compatibly\[1\].

* * *

TL;DR: agree.

* * *

1. Without a convoluted replacement bound that is essentially equivalent.

---

<div class="post-metadata">

**Author:** ![quinedot](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/quinedot/32/7294_2.png) [@quinedot](https://internals.rust-lang.org/u/quinedot)\
**Post date:** [December 30, 2024, 10:13pm UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/12 "2024-12-30T22:13:24Z")

</div>

> [@zirconium-n](#):
>
> I would really expect these two are exactly equivalent. It seems really hard to teach ☹.

> [@Ddystopia](#):
>
> Additionally, maybe some sort of mechanism to opt out of implicit behavior would help? For example, putting `#[not_imply_bounds]` on top of the trait declaration.

I had a footnote that didn't make it into the post that was along the lines of

> An attribute would be better than a different syntax, but we'll take what we can get

So I think we're in agreement here: something more explicit would be better. But like @steffahn said, for the existing functionality, it would need to be introduced over an edition to avoid taking away the choice.

> [@zirconium-n](#):
>
> I think the best case scenario is that: we don't need to spell out the additional where bounds, but we can't rely on it either (other than proving `T: Trait` itself).

Hmm. What does it mean to rely on it, though. For example, if you leave off the bound, can you call `method`?

```rust
pub trait Sub: Super where <Self as Super>::Assoc: Clone {
    fn method(&self, assoc: &Self::Assoc) {
        self.some_other_method(assoc.clone());
    }
    fn bar(&self) { /* ... */ }
}

```

If so, this is a breaking change.

```rust
pub trait Sub: Super {
    fn method(&self, assoc: &Self::Assoc) where <Self as Super>::Assoc: Clone {
        self.some_other_method(assoc.clone());
    }
    fn bar(&self) { /* ... */ }
}

```

("You can't call `method` because it uses the bound" isn't an adequate alternative IMO. I don't think anyone wants that kind of analysis anyway, but it would also making changing the default body to make use of the bound a breaking change.)

---

<div class="post-metadata">

**Author:** ![zirconium-n](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/zirconium-n/32/9078_2.png) [@zirconium-n](https://internals.rust-lang.org/u/zirconium-n)\
**Post date:** [December 31, 2024, 2:53am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/13 "2024-12-31T02:53:08Z")

</div>

Of course it's more complicated than I thought.

My instinct says "yes, you can call `method`", and "yes, of course adding a new bound is a breaking change". (I know technically it's less restricting than before, but `method` more restrict "out of context".)

---

<div class="post-metadata">

**Author:** ![Jules-Bertholet](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jules-bertholet/32/10671_2.png) [@Jules-Bertholet](https://internals.rust-lang.org/u/Jules-Bertholet)\
**Post date:** [January 16, 2025, 12:14am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/14 "2025-01-16T00:14:35Z")

</div>

I agree with this analysis.

* * *

However, I also agree that there are cases where a library might want to loosen a trait’s bounds, and it should be possible to do it backward-compatibly. Which is a primary motivation behind my [RFC 3437](https://github.com/rust-lang/rfcs/pull/3437).

---

<div class="post-metadata">

**Author:** ![system](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/system/32/14092_2.png) [@system](https://internals.rust-lang.org/u/system)\
**Post date:** [July 10, 2026, 12:15am UTC](https://internals.rust-lang.org/t/1-83-makes-removing-certain-trait-bounds-a-breaking-change/22089/15 "2026-07-10T00:15:19Z")

</div>

Closed on July 10, 2026, 12:15am UTC

This topic was automatically closed 540 days after the last reply. New replies are no longer allowed.
