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

Trait method impl restrictions #3678

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1a7ba23
Trait method impl restrictions
joshtriplett Jul 20, 2024
0c62bd8
RFC 3678
joshtriplett Aug 13, 2024
bbb6eaf
Future possibilities: `impl(unsafe)`
joshtriplett Aug 15, 2024
1e77b31
Add example of `Error::type_id` to motivation
joshtriplett Aug 20, 2024
25a53f9
Possible future work: integrate with stability markers
joshtriplett Aug 20, 2024
035b9f3
Move never-override syntax from future work to the body of the RFC
joshtriplett Aug 20, 2024
d6df9a3
Reword note about not placing in vtable
joshtriplett Sep 7, 2024
6bc226e
Switch to the `final` keyword
joshtriplett Sep 7, 2024
f3efb27
Rename the RFC
joshtriplett Sep 7, 2024
886a667
Future work: associated consts
joshtriplett Sep 10, 2024
60079a0
Add some further details
joshtriplett Sep 22, 2024
df3fc38
Avoid exhaustively enumerating qualifiers
joshtriplett Sep 22, 2024
79793f1
Word-wrap
joshtriplett Sep 22, 2024
13130e5
Reword descriptions of compatibility
joshtriplett Sep 22, 2024
476f4de
Clarifications about non-overridability of `final`
joshtriplett Sep 22, 2024
47eae02
Minor rewording to avoid being method-specific
joshtriplett Sep 22, 2024
931bd57
`final` has no impact on the `dyn`-compatibility of a trait
joshtriplett Sep 22, 2024
e91589f
Rephrase syntax explanation
joshtriplett Sep 22, 2024
a263847
Word-wrap and remove an extraneous detail
joshtriplett Sep 22, 2024
0c80693
Switch from `final` to `#[inherent]`
joshtriplett Oct 23, 2024
8964678
Add alternative/future possibility of inherent `impl Trait { ... }` b…
joshtriplett Oct 23, 2024
09fe432
For the alternative/future syntax, mention potential confusion with R…
joshtriplett Oct 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions text/3678-final.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
- Feature Name: `trait_inherent_items`
- Start Date: 2024-07-20
- RFC PR: [rust-lang/rfcs#3678](https://github.com/rust-lang/rfcs/pull/3678)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Support restricting implementation of individual methods within traits, using
an `#[inherent]` attribute.

# Motivation
[motivation]: #motivation

When defining a trait, the trait can provide optional methods with default
implementations, which become available on every implementation of the trait.
However, the implementer of the trait can still provide their own
implementation of such a method. In some cases, the trait does not want to
allow implementations to vary, and instead wants to guarantee that all
implementations of the trait use an identical method implementation. For
instance, this may be an assumption required for correctness.

This RFC allows restricting the implementation of trait methods.

This mechanism also faciliates marker-like traits providing no implementable
methods, such that implementers only choose whether to provide the trait and
never how to implement it; the trait then provides all the method
implementations.

One example of a trait in the standard library benefiting from this:
`Error::type_id`, which has thus far remained unstable because it's unsafe to
override. This RFC would allow stabilizing that method so users can call it,
without permitting reimplementation of it.

# Explanation
[explanation]: #explanation

When defining a trait, the definition can annotate methods or associated
functions to restrict whether implementations of the trait can define them. For
instance:

```rust
trait MyTrait: Display {
#[inherent]
fn method(&self) {
println!("MyTrait::method: {self}");
}
}
```

A method or associated function marked as `#[inherent]` must have a default body.

When implementing a trait, the compiler will emit an error if the
implementation attempts to define any method or associated function marked as
`#[inherent]`, and will emit a suggestion to delete the implementation.

In every other way, an `#[inherent]` method or associated function acts
identically to any other method or associated function, and can be invoked
accordingly:

```rust
fn takes_mytrait(m: &impl MyTrait) {
m.method();
}
```

Note that in some cases, the compiler might choose to avoid placing an
`#[inherent]` method in the trait's vtable, if the one-and-only implementation
does not benefit from monomorphization.

Note that removing an `#[inherent]` restriction is always forwards-compatible.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

At runtime, an `#[inherent] fn` behaves exactly the same as a `fn`.

Removing `#[inherent]` is always a non-breaking change. (If `#[inherent]` was
preventing implementation to prevent a soundness issue, though, this would
require additional care.)

Adding `#[inherent]` is a breaking change, unless the trait already did not
allow third-party implementations (such as via a sealed trait).

At compile-time, a method declared as `#[inherent] fn` in a trait must have a
provided body, and cannot be overridden in any `impl`, even an `impl` in the
same crate or module.

`#[inherent] fn` cannot be combined with `default fn`.

`#[inherent]` is only allowed in trait definitions. `#[inherent]` is not
allowed on impls or their items, non-trait functions, or `extern` blocks.

`#[inherent]` has no impact on the `dyn`-compatibility of a trait.

joshtriplett marked this conversation as resolved.
Show resolved Hide resolved
# Drawbacks
[drawbacks]: #drawbacks

As with any language feature, this adds more surface area to the language.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

Instead of or in addition to this, we could allow inherent `impl` blocks for a
`Trait` (e.g. `impl Trait { ... }` without `for Type`). People today already
occasionally write `impl dyn Trait` blocks, since `dyn Trait` is a type and
supports inherent impl blocks; this change would allow generalizing such blocks
by deleting the `dyn`. This has the potential for conceptual complexity or
confusion for new users, as well as potentially affecting the quality of
diagnostics. (It also used to have a meaning in Rust 2015: the same meaning
`impl dyn Trait` now has.) However, it would provide orthogonality, and an
interesting conceptual model.

Rather than using `#[inherent]`, we could use the `impl(visibility)` syntax
from [RFC 3323](https://rust-lang.github.io/rfcs/3323-restrictions.html). This
would allow more flexibility (such as overriding a method within the crate but
not outside the crate), and would be consistent with other uses of RFC 3323. On
the other hand, such flexibility would come at the cost of additional
complexity. We can always add such syntax for the more general cases in the
future if needed; see the future possibilities section.

Rather than using `#[inherent]`, we could use `#[final]` or `final`. This
concept is somewhat similar to "final" methods in other languages, and we
already have the `final` keyword reserved so we could use either an attribute
or a keyword. Using `#[inherent]` has the advantage of avoiding invoking an OO
concept that we don't fully match. It also evokes a family of similar concepts:
methods can be inherent to a type (not part of a trait), or inherent to a trait
(not part of a specific impl of the trait).

It's possible to work around the lack of this functionality by placing the
additional methods in an extension trait with a blanket implementation.
However, this is a user-visible API difference: the user must import the
extension trait, and use methods from the extension trait rather than from the
base trait.

# Prior art
[prior-art]: #prior-art

This feature is similar to `final` methods in Java or C++.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

We should consider this syntax and the inherent `impl Trait { ... }` syntax
(without `for Trait`) mentioned in the alternatives and future-possibilities
sections, and decide whether we want one or both of these. We should take into
account:
- The conceptual model we want to present to users
- Whether we anticipate user confusion due to the former meaning of this syntax in Rust 2015 (prior to the move from `Trait` to `dyn Trait` to write trait objects)
- Any effect on diagnostic quality
- Whether the two approaches differ in implementation complexity
- How much we want the benefit of allowing `impl dyn Trait` blocks to be
generalized by deleting the `dyn`

# Future possibilities
[future-possibilities]: #future-possibilities

As mentioned in the alternatives section, we could allow inherent `impl` blocks
for a `Trait` (e.g. `impl Trait { ... }` without `for Type`). People today
already occasionally write `impl dyn Trait` blocks, since `dyn Trait` is a type
and supports inherent impl blocks; this change would allow generalizing such
blocks by deleting the `dyn`.

We could add additional flexibility using the restriction mechanism defined in
[RFC 3323](https://rust-lang.github.io/rfcs/3323-restrictions.html), using
syntax like `impl(crate)` to restrict implementation of a method or associated
function outside a crate while allowing implementations within the crate.
(Likewise with `impl(self)` or any other visibility.)

We could theoretically allow `#[inherent]` restrictions on associated consts
and types, as well. If this is simple to implement, we should implement it for
all items that can appear in a trait simultaneously; if it proves difficult to
implement, we should prioritize methods.

We could support `impl(unsafe)`, to make a trait safe to implement if *not*
overriding the method, and only unsafe to implement if overriding the method.

We could integrate this with stability markers, to stabilize calling a method
but keep it unstable to *implement*.