Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local Default Bounds to assist Forget and other ?Trait. #3783

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

Ddystopia
Copy link

@Ddystopia Ddystopia commented Mar 1, 2025

This RFC proposes a mechanism for crates to define default bounds on generics. By specifying these defaults at the crate level we can reduce the need for verbose and repetitive ?Trait annotations while maintaining backward compatibility and enabling future language evolution.

It is targeted at supporting migrations from default being Trait to ?Trait, where Trait represents some assumption that is present everywhere but is not really exercised a lot, such as Forget, size = stride etc. Features like DynSized, as well as extern types, are out of the scope of this RFC, because it does not fit into this category. DynSized is not retracting mostly unexercised assumptions in order to make it ?DynSized the default.

Primary use case is Forget marker trait.

Rendered

Pre-RFC thread

@Noratrieb Noratrieb added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Mar 1, 2025
@Aloso
Copy link

Aloso commented Mar 7, 2025

Another drawback is that (similar to #[no_std] support and the MSRV) it is yet another thing for library authors to consider: Do I want to support ?Forget types?

  • If not, I can just ignore it and won't have any problems. But some users might not be able to use my library.
  • If I relax the bound by default, I might get compiler errors which I have to fix, causing churn and slowing down development. And once ?Forget bounds are part of the public API, making them !Forget again is a breaking change.

One situation where it could cause friction is when dependencies do not support ?Forget:

#![default_trait_bounds(?Forget)]

fn frobnicate(x: impl ToString) {
    other_crate::frobnicate(x); // error: `Forget` bound not satisfied
}

That said, I like this proposal and think that the benefits outweigh the drawbacks. Thank you!

[`DynSized`]: https://github.com/rust-lang/rfcs/pull/2984
[`extern types`]: https://github.com/rust-lang/rfcs/pull/1861

The syntax is to be bikeshedded, initially, it might be with a crate-level attributes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What’s the advantage of starting with this being a crate-level attribute rather than a module-level attribute?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think crate level attributes are enough to reach the goals, while module level attributes seem harder to implement to me.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough; I wanted to at least see the option of having module-level control acknowledged.

```rust
use std::ops::Deref;

trait Trait: Deref<Target: ?Forget> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did ?PartialEq disappear? Does default_generic_bounds override explicit trait bounds?? Wouldn't that make expressing ?Forget bounds inexpressible without changing the defaults, that feels like it mismatches with how ?Sized works, T: ?Sized overrides the default bound.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mistake indeed, previous version actually did that 😄

use std::ops::Deref;

trait Trait: Deref<Target: ?Forget> {
type Assoc;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this, shouldn't there be PartialEq mentioned here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it shouldn't. Only in functions that accept that trait would Assoc have a bound.


### Relax generic bound on public API

For migrated users it is equivalent to semver's `minor` change, while not migrated uses will observe it as `patch` change.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced by this, consider serde's Serialize trait :

pub trait Serialize {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
       where S: Serializer;
}

Relaxing the bound on S is a breaking change, because implementations must match the bounds exactly.

Similarly for impl Trait a library that provides a function fn foo() -> impl Trait can't relax that to fn foo() -> impl Trait + ?Forget as the callers may rely on the bounds as given.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. More work should be done on the rfc.


### Weakening associated type bound and `Self` bound in traits

Bounds for associated types and `Self` in traits would be weakened in respect to the new traits from the start:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By this do you mean that traits and associated types will ignore default_generic_bounds and always default to the most permissive possible bounds, and that this is fine because appropriate bounds will be enforced on struct and function generic arguments?

(I ask because it's not clear from the text what "the new traits" or "from the start" mean in the context of this RFC.)

Copy link
Author

@Ddystopia Ddystopia Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are correct. I will change the text to be clearer, thanks for the suggestion.


```rust
// crate `b` that has not migrated to `#![default_generic_bounds(?Forget)]`
mod b {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also mention dyn Trait, in argument position I think it's fine as it essentially acts like a generic bound (so you'd have to imply all the default trait bounds in there but it otherwise works. However, for return position (for example fn foo() -> Box<dyn Foo>) it has the same problem as impl Trait: it can't be relaxed because existing callers might depend on being able to (for example) forget it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants