Home Rust RFC 3634: Scoped impl Trait for Type
Post
Cancel

Rust RFC 3634: Scoped impl Trait for Type

I just posted my first Rust RFC. Hopefully it’s received well even though it’s on a relatively tough topic 😅

For posterity, you can find the full text I initially submitted below.
Non-grammar blockquotes are less-formal comments I added as further explanations.

I may edit this post eventually to include the correct Rust Issue link (if the RFC is merged), or to fix embarrassing typos, but for the most up-to-date version please see the ‘RFC PR’ linked just below this.

The rendered GitHub version may be a bit easier to navigate, too, as subheadings appear in the navigation there.
You can show the table of contents using the button at the very right of the sticky header on GitHub.

Thanks

  • to @teliosdev for some very early syntax feedback that helped put me on track,
  • to @cofinite for pointing out how scoped implementations allow syntax traits to be used as extension traits,
  • to @thefakeplace and to SkiFire13 in the draft discussion for suggestions on how to make this RFC more approachable and easier to understand.



  • Feature Name: scoped_impl_trait_for_type
  • Start Date: 2024-05-12
  • RFC PR: rust-lang/rfcs#3634
  • Rust Issue: (to be added if merged)

Summary

This proposal adds scoped impl Trait for Type items into the core language, as coherent but orphan-rule-free alternative to implementing traits globally. It also extends the syntax of use-declarations to allow importing these scoped implementations into other item scopes (including other crates), and differentiates type identity of most generics by which scoped trait implementations are available to each discretised generic type parameter (also adding syntax to specify differences to these captured implementation environments directly on generic type arguments).

This (along with some details specified below) enables any crate to

  • locally, in item scopes, implement nearly any trait for any expressible type,
  • publish these trivially composable implementations to other crates,
  • import and use such implementations safely and seamlessly and
  • completely ignore this feature when it’s not needed*.

* aside from one hopefully very obscure TypeId edge case that’s easy to accurately lint for.

This document uses “scoped implementation” and “scoped impl Trait for Type” interchangeably. As such, the former should always be interpreted to mean the latter below.

Motivation

While orphan rules regarding trait implementations are necessary to allow crates to add features freely without fear of breaking dependent crates, they limit the composability of third party types and traits, especially in the context of derive macros.

For example, while many crates support serde::{Deserialize, Serialize} directly, implementations of the similarly-derived bevy_reflect::{FromReflect, Reflect} traits are less common. Sometimes, a Debug, Clone or (maybe only contextually sensible) Default implementation for a field is missing to derive those traits. While crates like Serde often do provide ways to supply custom implementations for fields, this usually has to be restated on each such field. Additionally, the syntax for doing so tends to differ between derive macro crates.

Wrapper types, commonly used as workaround, add clutter to call sites or field types, and introduce mental overhead for developers as they have to manage distinct types without associated state transitions in order to work around the issues laid out in this section. They also require a distinct implementation for each combination of traits and lack discoverability through tools like rust-analyzer.

Another pain point are sometimes missing Into<>-conversions when propagating errors with ?, even though one external residual (payload) type may (sometimes contextually) be cleanly convertible into another. As-is, this usually requires a custom intermediary type, or explicit conversion using .map_err(|e| …) (or an equivalent function/extension trait). If an appropriate From<>-conversion can be provided in scope, then just ? can be used.

This RFC aims to address these pain points by creating a new path of least resistance that is easy to use and very easy to teach, intuitive to existing Rust-developers, readable without prior specific knowledge, discoverable as needed, has opportunity for rich tooling support in e.g. rust-analyzer and helpful error messages, is quasi-perfectly composable including decent re-use of composition, improves maintainability and (slightly) robustness to major-version dependency changes compared to newtype wrappers, and does not restrict crate API evolution, compromise existing coherence rules or interfere with future developments like specialisation. Additionally, it allows the implementation of more expressive (but no less explicit) extension APIs using syntax traits like in the PartialEq<>-example below, without complications should these traits be later implemented in the type-defining crate.

For realistic examples of the difference this makes, please check the rationale-and-alternatives section.

Guide-level explanation

Scoped impl Trait for Type can be introduced in The Book alongside global trait implementations and mentioned in the standard library documentation examples.

For example, the following changes could be made:

10.2. Traits: Defining Shared Behavior

The following sections are added after Implementing a Trait on a Type:

Scoped Implementation of a Trait on a Type

Independently of implementing a trait on a type or set of types globally, it’s possible to do so only for the current scope, by adding the use keyword:

1
2
3
use impl Trait for Type {
    // ...
}

With the exception of very few traits related to language features, you can implement any visible trait on any visible type this way, even if both are defined in other crates.

In other words: The orphan rule does not apply to scoped implementations. Instead, item shadowing is used to determine which implementation to use.

Scoped implementations are intended mainly as compatibility feature, to let third party crates provide glue code for other crate combinations. To change the behaviour of an instance or a set of instances from their default, consider using the newtype pattern instead.

Publishing and Importing Scoped Implementations

You can also publish a scoped implementation further by adding a visibility before use …:

1
2
3
4
5
6
7
pub use impl Trait for Type {
    // ...
}

pub use unsafe impl UnsafeTrait for Type {
    // ...
}

… and import it into other scopes:

1
2
3
4
use other_module::{
    impl Trait for Type,
    impl UnsafeTrait for Type,
};

Note that the scoped implementation of UnsafeTrait is imported without the unsafe keyword. It is the implementing crate’s responsibility to ensure the exported unsafe implementation is sound everywhere it is visible!

Generic parameters, bounds and where-clauses can be used as normal in each of these locations, though you usually have to brace impl</*...*/> Trait for Type where /*...*/ individually in use-declarations.

You can import a subset of a generic implementation, by narrowing bounds or replacing type parameters with concrete types in the use-declaration.

Global implementations can be imported from the root namespace, for example to shadow a scoped implementation:

1
use ::{impl Trait for Type};

Scoped implementations and generics

Scoped implementations are resolved on most generics’ type arguments where those are specified, and become part of the (now less generic) host type’s identity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#[derive(Default)]
struct Type<T>(T);

trait Trait {
    fn trait_fn();
}

impl<T: Trait> Type<T> {
    fn type_fn() {
        T::trait_fn();
    }
}

mod nested {
    use super::{Trait, Type};

    use impl Trait for () {
        fn trait_fn() {
            println!("nested");
        }
    }

    pub type Alias = Type<()>;
}
use nested::Alias;

fn main() {
    Alias::type_fn(); // "nested"

    // Type::<()>::type_fn();
    //             ^^^^^^^ error[E0599]: the function or associated item `type_fn` exists for struct `Type<()>`, but its trait bounds were not satisfied

    // let t: Type<()> = Alias::default();
    //                   ^^^^^^^^^ error[E0308]: mismatched types

    let t: Type<() as Trait in nested> = Alias::default();
}

This works equally not just for type aliases but also fields, let-bindings and also where generic type parameters are inferred automatically from expressions (for example to call a constructor).

Note that some utility types, like references, tuples, Option, Result and closure traits, do not bind implementations eagerly but only when used to specify another generic. You can find a list of these types in the reference. (← i.e. “insert link here”.)

19.2. Advanced Traits

The section Using the Newtype Pattern to Implement External Traits on External Types is updated to mention scoped implementations, to make them more discoverable when someone arrives from an existing community platform answer regarding orphan rule workarounds. It should also mention that newtypes are preferred over scoped implementations when use of the type is semantically different, to let the type checker distinguish it from others.

A new section is added:

Using Scoped Implementations to Implement External Traits on External Types

Since scoped implementations allow crates to reusably implement external traits on external types, they can be used to provide API extensions that make use of syntactic sugar. For example:

Filename: fruit-comparer/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use apples::Apple;
use oranges::Orange;

pub use impl PartialEq<Orange> for Apple {
    fn eq(&self, other: &Orange) -> bool {
        todo!("Figure out how to compare apples and oranges.")
    }
}

pub use impl PartialEq<Apple> for Orange {
    fn eq(&self, other: &Orange) -> bool {
        todo!("Figure out how to compare oranges and apples.")
    }
}

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use apples::Apple;
use oranges::Orange;

use fruit_comparer::{
    impl PartialEq<Orange> for Apple,
    impl PartialEq<Apple> for Orange,
};

fn main() {
    let apple = Apple::new();
    let orange = Orange::new();

    // Compiles:
    dbg!(apple == orange);
    dbg!(orange == apple);
}

If the type whose API was extended this way later gains the same trait inherently, that is not a problem as the consuming code continues to use fruit_comparer’s scoped implementation. However, a warning (global-trait-implementation-available) is shown by default to alert the maintainers of each crate of the covering global implementation.

Be careful about literal coercion when using generic traits this way! For example, if a scoped implementation of Index<isize> is used and a global Index<usize> implementation is added later on the same type, the compiler will not automatically decide which to use for integer literal indices between these two.

Rustdoc documentation changes

use and impl keywords

The documentation pages for the use keyword and for the impl keyword are adjusted to (very) briefly demonstrate the respective scoped use of impl Trait for Type.

TypeId

The page for TypeId gains two sections with the following information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# `TypeId` and scoped implementations

To make sure that that are no mix-ups between, for example, `HashSet<T>` and
`HashSet<T as Hash in module>`, any such difference implies distinct `TypeId`s between
such discretised generics (and that the types are not mutually assignable).

This also affects trait-bounded generic type parameters: If `T` is bounded on `Hash`, then
`TypeId::of::<T>()` results in distinct `TypeId`s in that context depending on the
captured implementation.

However, note that `TypeId::of::<T>()` and `TypeId::of::<T as Hash in module>()` are
always equivalent for one definition of `T`, as `TypeId::of`'s implementation does **not**
have a `T: Hash` bound!

For convenience (so that their values are easily interchangeable across crates), the
following types ignore scoped implementations *on* their generic arguments in terms of
*their own* type identity: […]

Despite this, differences in *type arguments'* discrete identities (for example from
scoped implementations captured *in* them) distinguish the type identity of *all*
discretised generics they appear in.

# `TypeId::of::<Self>()` may change for values of generics

To make type-erased collections sound and unsurprising by default, it's sound to transmute
between instances of an external generic type that differ only in their captured scoped
implementations, **iff and only iff** no inconsistency is ever observed by bounds
(including across separate function calls).

However, this poses a problem: `TypeId::of::<Self>()` (just like the written-out form of
any type that doesn't ignore scoped implementations) takes *all* differences in captured
implementation environments into account, not just those relevant to trait bounds.

As such, prefer `TypeId::of::<T>()` whenever possible in order to make only the
distinctions you require. You can use tuples to combine multiple type parameters without
over-distinguishing: `TypeId::of::<(S, T)>()`

These rules and the reasons for them are explained in detail in the reference-level-explanation below, as well as in logical-consistency as part of rationale-and-alternatives. It may be a good idea to link to similar longer explanations from the standard library docs above, even if just as “See also:”-style references for further reading.

The […]-placeholder stands for a list of links to each implementation-invariant generic’s documentation.

See also behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters for a way to narrowly alert users of this when relevant, and to scan for the potential impact of these changes ahead of time.

Implementation-invariant generics

The pages for implementation-invariant-generics gain a section similar to the following:

1
2
3
4
# Implementation-invariant generic

This type does not by itself capture scoped implementation environments when discretised.
See [`TypeId` and scoped implementations] for more information.

where [`TypeId` and scoped implementations] is a link to the section added to the TypeId page above.

mem::transmute

The page for transmute gains a section with the following information:

1
2
3
4
5
6
# `transmute` and scoped implementations

It is sound to transmute between discretised generic types that differ only in their
captured scoped implementation environments, **but only iff** such differences are
**never** observed by bounds on their implementation, including functions that imply such
by being implemented for discrete instances of the generic.

As far as I can tell, this is only practically relevant for certain kinds of type-erasing collections, like type-erasing hash maps and B-trees, of which I couldn’t find any examples on crates.io.

Any straightforward implementations of such collections should also at worst exhibit only unexpected behaviour when consumed in the presence of scoped implementations, rather than unsoundness.

Changes to The Rustonomicon

The page on Transmutes gains the following warning in addition to the existing ones:

1
2
3
4
5
6
- It is unsound to change [captured scoped implementations] via transmute for any external
  type if this change ever causes a contradiction observable by the transmuted value's
  implementation.

  This can happen due to bounds on called functions and/or because a called function is
  implemented for a specific type discretised from the generic.

[captured scoped implementations] should link to documentation introducing scoped impl Trait for Type.

Reference-level explanation

Grammar changes

The core Rust language grammar is extended as follows:

  • TraitImpl’s definition is prepended with (Visibility? use)? and refactored for partial reuse to arrive at

    TraitImpl :
    (Visibility? use)? unsafe? TraitCoverage
    {
       InnerAttribute*
       AssociatedItem*
    }

    TraitCoverage :
    TraitCoverageNoWhereClause
    WhereClause?

    TraitCoverageNoWhereClause :
    impl GenericParams? !? TypePath for Type

    where a trait implementation with that use-prefix provides the implementation only as item in the containing item scope.

    (This can be distinguished from use-declarations with a lookahead up to and including impl or unsafe, meaning at most four shallowly tested token trees with I believe no groups. No other lookaheads are introduced into the grammar by this RFC.)

    The scoped implementation defined by this item is implicitly always in scope for its own definition. This means that it’s not possible to refer to any shadowed implementation inside of it (including generic parameters and where clauses), except by re-importing specific scoped implementations inside nested associated functions. Calls to generic functions cannot be used as backdoor either (see type-parameters-capture-their-implementation-environment).

  • UseTree’s definition is extended for importing scoped implementations by inserting the extracted TraitCoverage and TraitCoverageNoWhereClause rules as follows:

    UseTree :
      (SimplePath? ::)? *
      | (SimplePath? ::)? {
      (
       ((UseTree | TraitCoverageNoWhereClause) (, (UseTree | TraitCoverageNoWhereClause))* (, TraitCoverage?)?)?
       | TraitCoverage
      )
    }
      | SimplePath (as (IDENTIFIER | _))?

    Allowing a trailing TraitCoverage with WhereClause in a braced list is intended for ergonomics, but rustfmt should brace it individually by default, then append a trailing comma where applicable as usual. A ‘,’ in the WhereClause here is not truly ambiguous because WhereClauseItems contain ‘:’, but allowing that ahead of others would likely be visually confusing and tricky to implement (requiring an arbitrarily long look-ahead). Alternatively to allowing a trailing TraitCoverage in mixed lists, an error similar to E0178 could be emitted.

    Allowing unbraced imports like use some_crate::impl<A, B> Trait<A> for Type<B> where A: Debug, B: Debug; would break the source code’s visual hierarchy quite badly, so I won’t suggest it here, but it is possible without ambiguity too. If that is added for convenience, then I’m strongly in favour of rustfmt bracing the TraitCoverage by default and rust-analyzer suggesting it only braced.

    Here, TraitCoverage imports the specified scoped impl Trait for Type for binding and conflict checks as if defined in the scope containing the use-declaration. The resulting visibility is taken from UseDeclaration, like with SimplePath-imported items.

    TraitCoverage must be fully covered by the scoped implementation visible in the source module. Otherwise, a compile-error occurs explaining the uncovered case (similarly to the current error(s) for missing trait implementations).

    TraitCoverage may subset the source module’s implementation by having narrower bounds or using concrete types in place of one or more generic type parameters. This causes only the specified subset of the scoped implementation to be imported.

    Note that scoped implementations of unsafe traits are imported without unsafe. It is the exporting crate’s responsibility to ensure a scoped implementation is sound everywhere it is visible.

    Other elements of the coverage must match the source module’s implementation exactly, unless specified otherwise.

  • TypeParam, GenericArg and GenericArgsBinding are extended to accept implementation environments inline:

    TypeParam :
      IDENTIFIER ( : TypeParamBounds? )? ( = Type ImplEnvironment? )?

    GenericArg :
    Lifetime | Type ImplEnvironment? | GenericArgsConst | GenericArgsBinding

    GenericArgsBinding :
      IDENTIFIER = Type ImplEnvironment?

    ImplEnvironment :
    as ( ImplEnvironmentEntry ( + ImplEnvironmentEntry )* +? )?

    ImplEnvironmentEntry :
      (
       ForLifetimes? TypePath
       | ( ForLifetimes? TypePath )
      )
    in ( :: | SimplePath )

    When detecting conflicting implementations, the ImplEnvironment is treated as creating a distinct scope nested in its surrounding scope. Each resulting implementation environment must be conflict-free, but between them they can contain conflicting implementations.

    Even when an ImplEnvironment is added as above, the resulting implementation environment still captures scoped implementations from the surrounding scope for all traits that were not specified inline! A global implementation can be used explicitly by sourcing it from :: instead of a module.

    For stability reasons (against relaxation of bounds) and because they matter for type identity, explicit inline implementation environments should be allowed where no matching bound is present, but should produce an unused-scoped-implementation warning iff neither published nor used in the same crate (including for type identity distinction).

    Whether inline implementation environments would inherit from each other is intentionally left unspecified, as identical types can’t be nested without indirection, which ensures such a situation isn’t relevant.

  • Further type specification syntax is extended as follows:

    ParenthesizedType :
    ( Type ImplEnvironment? )

    TupleType :
    ( )
      | ( ( Type ImplEnvironment? , )+ ( Type ImplEnvironment? )? )

    ArrayType :
    [ Type ImplEnvironment? ; Expression ]

    SliceType :
    [ Type ImplEnvironment? ]

    Closure types are not extended with ImplEnvironment because implementation environments annotated on their parameters would never be effective.

    Extending ParenthesizedType this way is necessary to specify implementation environments for pointer types’ generic type parameters, e.g. &(Type as Trait in module).

  • QualifiedPathType is also extended for this purpose, but can additionally act as implementation environment scope that also affects the implementation environment of nested types, using a clause starting with where:

    QualifiedPathType :
    < Type ( as TypePath (in (:: | SimplePath ) )? )? ( where ( Type ImplEnvironment , )* ( Type ImplEnvironment )? )? >

    The form <Type as Trait in module> is syntactic sugar for <(Type as Trait in module) as Trait>, to avoid repetition of potentially long traits.

    Implementations imported after where must be valid, but don’t necessarily have to be relevant.

    I am not confident that where is the right keyword here, but it seems like this best option among the already-existing ones. use-syntax feels far too verbose here. Maybe the above but with using or with in place of where?

No scoped impl Trait for Type of auto traits, Copy and Drop

Implementations of auto traits state guarantees about private implementation details of the covered type(s), which an external implementation can almost never do soundly.

Copy is not an auto trait, but implementing it on a smart pointer like Box<T> would immediately be unsound. As such, this trait must be excluded from all external implementations.

Shadowing Drop for types that are !Unpin is similarly unsound without cooperation of the original crate (in addition to likely causing memory leaks in this and more cases).

No scoped impl !Trait for Type

Any negative scoped implementation like for example

1
use impl !Sync for Type {}

is syntactically valid, but rejected by the compiler with a specific error. (See negative-scoped-implementation.)

This also applies to impl Traits in use-declarations (even though the items they would import cannot be defined anyway. Having a specific error saying that this isn’t possible would be much clearer than one saying that the imported item doesn’t exist).

No external scoped implementations of sealed traits

Consider this library crate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct Generic<T>(T);

mod private {
    // Implemented only on traits that are also `Sealed`.
    pub trait Sealing {}
}
use private::Sealing;

pub trait Sealed: Sealing {
    fn assumed {
        // ❷
    }
}

impl<T: Sealed> Generic {
    fn assuming {
        // ❶
    }
}

In this crate, any code at ❶ is currently allowed to make safety-critical assumptions about code at ❷ and other implementations of assumed.

To ensure this stays sound, scoped impl Trait for Type where Trait is external requires that all supertraits of Trait are visible to the crate defining the scoped implementation or are defined not in Trait’s definition crate (meaning they must still be exported from a crate somewhere in the dependency tree).

See also scoped-implementation-of-external-sealed-trait.

Type parameters capture their implementation environment

When a type parameter is specified, either explicitly or inferred from an expression, it captures a view of all implementations that are applicable to its type there. This is called the type parameter’s implementation environment.

(For trait objects, associated types are treated as type parameters for the purposes of this proposal.)

When implementations are resolved on the host type, bounds on the type parameter can only be satisfied according to this captured view. This means that implementations on generic type parameters are ‘baked’ into discretised generics and can be used even in other modules or crates where this discretised type is accessible (possibly because a value of this type is accessible). Conversely, additional or changed implementations on a generic type parameter in an already-discretised type cannot be provided anywhere other than where the type parameter is specified.

When a generic type parameter is used to discretise another generic, the captured environment is the one captured in the former but overlaid with modifications applicable to that generic type parameter’s opaque type.

Note that type parameter defaults too capture their implementation environment where they are specified, so at the initial definition site of the generic. This environment is used whenever the type parameter default is used.

In order to avoid too much friction, implementation-invariant-generics are exempt from acting as host for implementation environments on their own.

Type identity of discrete types

The type identity and TypeId::of::<…>() of discrete types, including discretised generics, are not affected by scoped implementations on them.

Type identity of generic types

Implementation-aware generics

Generics that are not implementation-invariant-generics are implementation-aware generics.

The type identity of implementation-aware generic types is derived from the types specified for their type parameters as well as the full implementation environment of each of their type parameters and their associated types:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#[derive(Default)]
struct Type;
#[derive(Default)]
struct Generic<T>(T);
trait Trait {}

impl<T> Generic<T> {
    fn identical(_: Self) {}
    fn nested_convertible<U: Into<T>>(_: Generic<U>) {}
}

mod mod1 {
    use crate::{Generic, Trait, Type};
    use impl Trait for Type {} // Private implementation, but indirectly published through `Alias1`.
    pub type Alias1 = Generic<Type>;
}

mod mod2 {
    use crate::{Generic, Trait, Type};
    pub use impl Trait for Type {} // Public implementation.
    pub type Alias2 = Generic<Type>;
}

mod mod3 {
    use crate::{Generic, Trait, Type};
    use crate::mod2::{impl Trait for Type}; // Reused implementation.
    pub type Alias3 = Generic<Type>;
}

mod mod4 {
    use crate::{Generic, Trait, Type};
    use impl<T> Trait for Generic<T> {} // Irrelevant top-level implementation.
    pub type Alias4 = Generic<Type>;
}

mod mod5 {
    use crate::{Generic, Type};
    // No implementation.
    pub type Alias5 = Generic<Type>;
}

use mod1::Alias1;
use mod2::Alias2;
use mod3::Alias3;
use mod4::Alias4;
use mod5::Alias5;

fn main() {
    use std::any::TypeId;

    use tap::Conv;

    // Distinct implementations produce distinct types.
    assert_ne!(TypeId::of::<Alias1>(), TypeId::of::<Alias2>());
    assert_ne!(TypeId::of::<Alias1>(), TypeId::of::<Alias3>());

    // Types with identical captured implementation environments are still the same type.
    assert_eq!(TypeId::of::<Alias2>(), TypeId::of::<Alias3>());

    // Top-level implementations are not part of type identity.
    assert_eq!(TypeId::of::<Alias4>(), TypeId::of::<Alias5>());

    // If the type is distinct, then values aren't assignable.
    // Alias1::identical(Alias2::default());
    //                   ^^^^^^^^^^^^^^^^^ error[E0308]: mismatched types

    // Fulfilled using the global reflexive `impl<T> Into<T> for T` on `Type`,
    // as from its perspective, the binding is stripped due to being top-level.
    Alias1::nested_convertible(Alias2::default());

    // The reflexive `impl<T> Into<T> for T` does not apply between the aliases here,
    // as the distinct capture in the type parameter affects its inherent identity.
    // (It's unfortunately not possible to generically implement this conversion without specialisation.)
    // Alias1::default().conv::<Alias2>();
    //                   ^^^^ error[E0277]: the trait bound `Generic<Type as Trait in mod2>: From<Generic<Type as Trait in mod1>>>` is not satisfied

    // Identical types are interchangeable.
    Alias2::identical(Alias3::default());
    Alias4::identical(Alias5::default());
}

As mentioned in type-identity-of-discrete-types, implementations on the generic type itself do not affect its type identity, as can be seen with Alias4 above.

The TypeId of these generics varies alongside their identity. Note that due to the transmutation permission defined in layout-compatibility, consumer code is effectively allowed to change the TypeId of instances of generics between calls to generic implementations in most cases. Due to this, implementations of generics that manage types at runtime should usually rely on the typeid-of-generic-type-parameters-opaque-types or (…,)-tuple-types combining them instead of on TypeId::of::<Self>(). (see also behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters)

(For a practical example, see logical-consistency.)

Implementation-invariant generics

The following generics that never rely on the consistency of trait implementations on their type parameters are implementation-invariant:

  • &T, &mut T (references),
  • *const T, *mut T (pointers),
  • [T; N], [T] (arrays and slices),
  • (T,), (T, U, ..) (tuples),
  • superficially* fn(T) -> U and similar (function pointers),
  • superficially* Fn(T) -> U, FnMut(T) -> U, FnOnce(T) -> U, Future<Output = T>, Iterator<Item = T>, std::ops::Coroutine and similar (closures),
  • Pin<P>, NonNull<T>, Box<T>, Rc<T>, Arc<T>, Weak<T>, Option<T>, Result<T, E>**.

Implementation-invariant generics never capture implementation environments on their own. Instead, their effective implementation environments follow that of their host, acting as if they were captured in the same scope.

The type identity of implementation-invariant generics seen on their own does not depend on the implementation environment. This also means that the TypeId of Option<T> does not take into account differences of implementations on T. However, differences of implementations in T can still distinguish the types, in cases where the type identity (and possibly TypeId) of T itself are different. An example for this are generic type parameters’ effective types that can have bounds-relevant implementations observably baked into them.

Hosts are:

* superficially: The underlying instance may well use a captured implementation internally, but this isn’t surfaced in signatures. For example, a closure defined where usize: PartialOrd in reverse + Ord in reverse is just FnOnce(usize) but will use usize: PartialOrd in reverse + Ord in reverse privately when called.

** but see which-structs-should-be-implementation-invariant.

See also why-specific-implementation-invariant-generics.

Call expressions’ function operand captures its implementation environment

Call expressions capture the implementation environment in their function operand, acting as host for implementation-invariant-generics. This enables call expressions such as

1
Option::<Type as Debug in module>::fmt()

where fmt receives the specified scoped implementation by observing it through the T: Debug bound on its implementing impl block.

If no observing bound exists, code of this form should produce a warning spanning the Trait in module tokens. (see unused-scoped-implementation)

Type aliases are opaque to scoped implementations

As scoped impl Trait for Type is a fully lexically-scoped feature, the implementation environment present in a scope does not affect types hidden behind a type alias, except for the top-level type directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
trait Trait {
    fn method(&self) -> &str;
}

impl Trait for Type {
    fn method(&self) -> &str {
        "global"
    }
}

mod m1 {
    use super::Type;

    pub type Alias = [Type; 1];
}

mod m2 {
    use super::{Type, Trait};

    pub use impl Trait for Type {
        fn method(&self) -> &str {
            "scoped"
        }
    }

    pub use impl<T: Trait> Trait for [T; 1] {
        fn method(&self) -> &str {
            self[0].method()
        }
    }
}

fn main() {
    use m1::Alias;
    use m2::{
        impl Trait for Type,
        impl Trait for [Type; 1],
    };

    assert_eq!([Type].method(), "scoped");
    assert_eq!(Alias::default().method(), "global");
}

Scoped implementations may still be observed through bounded generic type parameters on the type alias itself. (see binding-choice-by-implementations-bounds)

TypeId of generic type parameters’ opaque types

In addition to the type identity of the specified type, the TypeId of opaque generic type parameter types varies according to the captured implementation environment, but only according to implementations that are relevant to their bounds (including implicit bounds), so that the following program runs without panic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use std::any::TypeId;

#[derive(Default)]
struct Type;
trait Trait {}
impl Trait for Type {}

#[derive(Default)]
struct Generic<T>(T);

mod nested {
    pub(super) use impl super::Trait for super::Type {}
}

// `A` and `B` are distinct due to different captured implementation environments.
type A = Generic<Type>;
type B = Generic<Type as Trait in nested>;

fn no_bound<T: 'static, U: 'static>(_: Generic<T>, _: Generic<U>) {
    assert_eq!(TypeId::of::<T>(), TypeId::of::<U>());
    assert_ne!(TypeId::of::<Generic<T>>(), TypeId::of::<Generic<U>>());

    assert_eq!(TypeId::of::<T>(), TypeId::of::<Type>());
    assert_eq!(TypeId::of::<U>(), TypeId::of::<Type>());
}

fn yes_bound<T: Trait + 'static, U: Trait + 'static>(_: Generic<T>, _: Generic<U>) {
    assert_ne!(TypeId::of::<T>(), TypeId::of::<U>());
    assert_ne!(TypeId::of::<Generic<T>>(), TypeId::of::<Generic<U>>());

    assert_eq!(TypeId::of::<T>(), TypeId::of::<Type>());
    assert_ne!(TypeId::of::<U>(), TypeId::of::<Type>());
}

fn main() {
    no_bound(A::default(), B::default());
    yes_bound(A::default(), B::default());
}

In particular:

  • If no bound-relevant scoped implementations are captured in a type parameter, then the TypeId of the opaque type of that type parameter is identical to that of the discrete type specified for that type parameter.
  • Distinct sets of bound-relevant captured scoped implementations lead to distinct TypeIds of the opaque type of a type parameter.
  • If the set of bound-relevant captured scoped implementations in two generic type parameters is the same, and the wrapped discrete type is identical, then the TypeId of the opaque types of these generic type parameters is identical.
  • If a generic type parameter is distinguishable this way, it remains distinguishable in called implementations even if those have fewer bounds - the relevant distinction is ‘baked’ into the generic type parameter’s opaque type.

These rules (and the transmutation permission in layout-compatibility) allow the following collection to remain sound with minimal perhaps unexpected behaviour:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
use std::{
    any::TypeId,
    collections::{
        hash_map::{HashMap, RandomState},
        HashSet,
    },
    hash::{BuildHasher, Hash},
    mem::drop,
};

use ondrop::OnDrop;

#[derive(Default)]
pub struct ErasedHashSet<'a, S: 'a + BuildHasher + Clone = RandomState> {
    storage: HashMap<TypeId, *mut (), S>,
    droppers: Vec<OnDrop<Box<dyn FnOnce() + 'a>>>,
}

impl ErasedHashSet<'_, RandomState> {
    pub fn new() -> Self {
        Self::default()
    }
}

impl<'a, S: BuildHasher + Clone> ErasedHashSet<'a, S> {
    pub fn with_hasher(hasher: S) -> Self {
        Self {
            storage: HashMap::with_hasher(hasher),
            droppers: vec![],
        }
    }

    // This is the important part.
    pub fn insert<T: 'a>(&mut self, value: T) -> bool
    where
        T: Hash + Eq + 'static, // <-- Bounds.
    {
        let type_id = TypeId::of::<T>(); // <-- `TypeId` depends on implementations of bounds.
        let storage: *mut () = if let Some(storage) = self.storage.get_mut(&type_id) {
            *storage
        } else {
            let pointer = Box::into_raw(Box::new(HashSet::<T, S>::with_hasher(
                self.storage.hasher().clone(),
            )));
            self.droppers.push(OnDrop::new(Box::new(move || unsafe {
                // SAFETY: Only called once when the `ErasedHashSet` is dropped.
                //         The type is still correct since the pointer wasn't `.cast()` yet and
                //         both `S` and `T` are bounded on `'a`, so they are still alive at this point.
                drop(Box::from_raw(pointer));
            })));
            self.storage
                .insert(type_id, pointer.cast::<()>())
                .expect("always succeeds")
        };

        let storage: &mut HashSet<T, S> = unsafe {
            // SAFETY: Created with (close to) identical type above.
            //         Different `Hash` and `Eq` implementations are baked into `T`'s identity because of the bounds, so they result in distinct `TypeId`s above.
            //         It's allowed to transmute between types that differ in identity only by bound-irrelevant captured implementations.
            //         The borrowed reference isn't returned.
            &mut *(storage.cast::<HashSet<T, S>>())
        };
        storage.insert(value)
    }

    // ...
}

In particular, this code will ignore any scoped implementations on T that are not Hash, Eq or (implicitly) PartialEq, while any combination of distinct discrete type and implementation environments with distinct Hash, Eq or PartialEq implementations is cleanly separated.

See also behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters for how to lint for an implementation of this collection that uses TypeId::of::<HashSet<T, S>>() as key, which also remains sound and deterministic but distinguishes too aggressively by irrelevant scoped implementations in consumer code, leading to unexpected behaviour.

(For an example of TypeId behaviour, see logical-consistency.)

Layout-compatibility

Types whose identities are only distinct because of a difference in implementation environments remain layout-compatible as if one was a #[repr(transparent)] newtype of the other.

It is sound to transmute an instance between these types if no inconsistency is observed on that instance by the bounds of any external-to-the-transmute implementation or combination of implementations, including scoped implementations and implementations on discrete variants of the generic. As a consequence, the Self-observed TypeId of instances of generic types may change in some cases.

For example, given a library

1
2
3
4
5
6
#[derive(Debug)]
pub struct Type<T>(T);

impl Type<usize> {
    pub fn method(&self) {}
}

then in another crate

  • if Debug is used on an instance of Type<T>, then this instance may not be transmuted to one where T: Debug uses a different implementation and have Debug used on it again afterwards, and
  • if Type<usize>::method() is used on an instance of Type<usize>, then that instance may not be transmuted (and used) to or from any other variant, including ones that only differ by captured implementation environment, because method has observed the exact type parameter through its constraints.

(In short: Don’t use external-to-your-code implementations with the instance in any combination that wouldn’t have been possible without transmuting the instance, pretending implementations can only observe the type identity according to their bounds.)

See typeid-of-generic-type-parameters-opaque-types for details on what this partial transmutation permission is for, and behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters for a future incompatibility lint that could be used to warn implementations where this is relevant.

No interception/no proxies

That each scoped impl Trait for Type { /*...*/ } is in scope for itself makes the use of the implementation it shadows in the consumer scope inexpressible. There can be no scoped implementation constrained to always shadow another.

This is intentional, as it makes the following code trivial to reason about:

1
2
3
4
5
6
7
{
    use a::{impl TheTrait for TheType}; // <-- Clearly unused, no hidden interdependencies.
    {
        use b::{impl TheTrait for TheType};
        // ...
    }
}

(The main importance here is to not allow non-obvious dependencies of imports. Implementations can still access associated items of a specific other implementation by bringing it into a nested scope or binding to its associated items elsewhere. See also independent-trait-implementations-on-discrete-types-may-still-call-shadowed-implementations.)

Binding choice by implementations’ bounds

Implementations bind to other implementations as follows:

where-clause¹ on impl?binding-site of used traitmonomorphised by used trait?
Yes.Bound at each binding-site of impl.Yes, like-with or as-part-of type parameter distinction.
No.Bound once at definition-site of impl.No.

¹ Or equivalent generic type parameter bound, where applicable. For all purposes, this RFC treats them as semantically interchangeable.

A convenient way to think about this is that impl-implementations with bounds are blanket implementations over Self in different implementation environments.

Note that Self-bounds on associated functions do not cause additional monomorphic variants to be emitted, as these continue to only filter the surrounding implementation.

Consider the following code with attention to the where clauses:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
struct Type;

// ❶

trait Trait { fn function(); }
impl Trait for Type { fn function() { println!("global"); } }

trait Monomorphic { fn monomorphic(); }
impl Monomorphic for Type {
    fn monomorphic() { Type::function() }
}

trait MonomorphicSubtrait: Trait {
    fn monomorphic_subtrait() { Self::function(); }
}
impl MonomorphicSubtrait for Type {}

trait Bounded { fn bounded(); }
impl Bounded for Type where Type: Trait {
    fn bounded() { Type::function(); }
}

trait BoundedSubtrait: Trait {
    fn bounded_subtrait() { Type::function(); }
}
impl BoundedSubtrait for Type where Type: Trait {}

trait FnBoundedMonomorphic {
    fn where_trait() where Self: Trait { Self::function(); }
    fn where_monomorphic_subtrait() where Self: MonomorphicSubtrait { Self::monomorphic_subtrait(); }
}
impl FnBoundedMonomorphic for Type {}

trait NestedMonomorphic { fn nested_monomorphic(); }

trait BoundedOnOther { fn bounded_on_other(); }
impl BoundedOnOther for () where Type: Trait {
    fn bounded_on_other() { Type::function(); }
}

Type::function(); // "global"
Type::monomorphic(); // "global"
Type::monomorphic_subtrait(); // "global"
Type::bounded(); // "global"
Type::bounded_subtrait(); // "global"
Type::where_trait(); // "global"
Type::where_monomorphic_subtrait(); // "global"
Type::nested_monomorphic(); // "scoped"
()::bounded_on_other(); // "global"

{
    // ❷
    use impl Trait for Type {
        fn function() {
            println!("scoped");
        }
    }

    // use impl FnBoundedMonomorphic for Type {}
    // error: the trait bound `Type: MonomorphicSubtrait` is not satisfied

    Type::function(); // "scoped"
    Type::monomorphic(); // "global"
    // Type::monomorphic_subtrait(); // error; shadowed by scoped implementation
    Type::bounded(); // "scoped"
    Type::bounded_subtrait(); // "scoped"
    Type::where_trait(); // "global"
    Type::where_monomorphic_subtrait(); // "global"
    Type::nested_monomorphic(); // "scoped"
    ()::bounded_on_other(); // "global"

    {
        // ❸
        use impl MonomorphicSubtrait for Type {}
        use impl FnBoundedMonomorphic for Type {}

        impl NestedMonomorphic for Type {
            fn nested_monomorphic() { Type::function() }
        }

        Type::function(); // "scoped"
        Type::monomorphic(); // "global"
        Type::monomorphic_subtrait(); // "scoped"
        Type::bounded(); // "scoped"
        Type::bounded_subtrait(); // "scoped"
        Type::where_trait(); // "scoped"
        Type::where_monomorphic_subtrait(); // "scoped"
        Type::nested_monomorphic(); // "scoped"
        ()::bounded_on_other(); // "global"
    }
}

The numbers ❶, ❷ and ❸ mark relevant item scopes.

Generic item functions outside impl blocks bind and behave the same way as generic impls with regard to scoped impl Trait for Type.

Trait / ::function

This is a plain monomorphic implementation with no dependencies. As there is a scoped implementation at ❷, that one is used in scopes ❷ and ❸.

Monomorphic / ::monomorphic

Another plain monomorphic implementations.

As there is no bound, an implementation of Trait is bound locally in ❶ to resolve the Type::function()-call.

This means that even though a different use impl Trait for Type … is applied in ❷, the global implementation remains in use when this Monomorphic implementation is called into from there and ❸.

Note that the use of Self vs. Type in the non-default function body does not matter at all!

MonomorphicSubtrait / ::monomorphic_subtrait

Due to the supertrait, there is an implied bound Self: Trait on the trait definition, but not on the implementation.

This means that the implementation remains monomorphic, and as such depends on the specific (global) implementation of Trait in scope at the impl MonomorphicSubtrait … in ❶.

As this Trait implementation is shadowed in ❷, the MonomorphicSubtrait implementation is shadowed for consistency of calls to generics bounded on both traits.

In ❸ there is a scoped implementation of MonomorphicSubtrait. As the default implementation is monomorphised for this implementation, it binds to the scoped implementation of Trait that is in scope here.

Bounded / ::bounded

The Type: Trait bound (can be written as Self: Trait – they are equivalent.) selects the Bounded-binding-site’s Type: Trait implementation to be used, rather than the impl Bounded for …-site’s.

In ❶, this resolves to the global implementation as expected.

For the scopes ❷ and ❸ together, Bounded gains one additional monomorphisation, as here another Type: Trait is in scope.

BoundedSubtrait / ::bounded_subtrait

As with MonomorphicSubtrait, the monomorphisation of impl BoundedSubtrait for Type … that is used in ❶ is shadowed in ❷.

However, due to the where Type: Trait bound on the implementation, that implementation is polymorphic over Trait for Type implementations. This means a second monomorphisation is available in ❷ and its nested scope ❸.

FnBoundedMonomorphic

FnBoundedMonomorphic’s implementations are monomorphic from the get-go just like Monomorphic’s.

Due to the narrower bounds on functions, their availability can vary between receivers but always matches that of the global implementation environment:

::where_trait

Available everywhere since Type: Trait is in scope for both implementations of FnBoundedMonomorphic.

In ❶, this resolves to the global implementation.

In ❷, this still calls the global <Type as Trait in ::>::function() implementation since the global FnBoundedMonomorphic implementation is not polymorphic over Type: Trait.

In ❸, FnBoundedMonomorphic is monomorphically reimplemented for Type, which means it “picks up” the scoped Type: Trait implementation that’s in scope there from ❷.

::where_monomorphic_subtrait

In ❶, this resolves to the global implementation.

In ❷, this still calls the global <Type as MonomorphicSubtrait in ::>::monomorphic_subtrait() implementation since the global FnBoundedMonomorphic implementation is not polymorphic over Type: Trait.

Note that FnBoundedMonomorphic cannot be reimplemented in ❷ since the bound Type: MonomorphicSubtrait on its associated function isn’t available in that scope, which would cause a difference in the availability of associated functions (which would cause a mismatch when casting to dyn FnBoundedMonomorphic).

It may be better to allow use impl FnBoundedMonomorphic for Type {} without where_monomorphic_subtrait in ❷ and disallow incompatible unsizing instead. I’m not sure about the best approach here.

In ❸, FnBoundedMonomorphic is monomorphically reimplemented for Type, which means it “picks up” the scoped Type: Trait implementation that’s in scope there from ❷.

NestedMonomorphic / ::nested_monomorphic

The global implementation of NestedMonomorphic in ❸ the binds to the scoped implementation of Trait on Type from ❷ internally. This allows outside code to call into that function indirectly without exposing the scoped implementation itself.

BoundedOnOther / ::bounded_on_other

As this discrete implementation’s bound isn’t over the Self type (and does not involved generics), it continues to act only as assertion and remains monomorphic.

Binding and generics

where-clauses without generics or Self type, like where (): Debug, do not affect binding of implementations within an impl or fn, as the non-type-parameter-type () is unable to receive an implementation environment from the discretisation site.

However, where (): From<T> does take scoped implementations into account because the blanket impl<T, U> From<T> for U where T: Into<U> {} is sensitive to T: Into<()> which is part of the implementation environment captured in T!

This sensitivity even extends to scoped use impl From<T> for () at the discretisation site, as the inverse blanket implementation of Into creates a scoped implementation of Into wherever a scoped implementation of From exists.
This way, existing symmetries are fully preserved in all contexts.

Implicit shadowing of subtrait implementations

Take this code for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use std::ops::{Deref, DerefMut};

struct Type1(Type2);
struct Type2;

impl Deref for Type1 {
    type Target = Type2;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Type1 {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

fn function1(_x: impl Deref + DerefMut) {}
fn function2(x: impl DerefMut) {
    x.deref();
}

{
    use impl Deref for Type1 {
        type Target = ();

        fn deref(&self) -> &Self::Target {
            &()
        }
    }

    // function1(Type1(Type2)); // <-- Clearly impossible.
    // function2(Type1(Type2)); // <-- Unexpected behaviour if allowed.
}

Clearly, function1 cannot be used here, as its generic bounds would have to bind to incompatible implementations.

But what about function2? Here, the bound is implicit but Deref::deref could still be accessed if the function could be called. For type compatibility, this would have to be the shadowed global implementation, which is most likely unintended decoherence.

As such, shadowing a trait implementation also shadows all respective subtrait implementations. Note that the subtrait may still be immediately available (again), if it is implemented with a generic target and all bounds can be satisfied in the relevant scope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
trait Trait1 {
    fn trait1(&self);
}
trait Trait2: Trait1 { // <-- Subtrait of Trait1.
    fn uses_trait1(&self) {
        self.trait1();
    }
}
impl<T: Trait1> Trait2 for T {} // <-- Blanket implementation with bounds satisfiable in scope.

struct Type;
impl Trait1 for Type {
    fn trait1(&self) {
        print!("global");
    }
}

{
    use impl Trait1 for Type {
        fn trait1(&self) {
            print!("scoped");
        }
    }

    Type.uses_trait1(); // Works, prints "scoped".
}

If a subtrait implementation is brought into scope, it must be either an implementation with a generic target, or an implementation on a discrete type making use of the identical supertrait implementations in that scope. (This rule is automatically fulfilled by scoped implementation definitions, so it’s only relevant for which scoped implementations can be imported via use-declaration.)

Independent trait implementations on discrete types may still call shadowed implementations

Going back to the previous example, but now implementing Trait2 independently without Trait1 in its supertraits:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
trait Trait1 {
    fn trait1(&self);
}
trait Trait2 { // <-- Not a subtrait of `Trait1`.
    fn uses_trait1(&self);
}
impl Trait2 for Type { // <-- Implementation on discrete type.
    fn uses_trait1(&self) {
        self.trait1();
    }
}

struct Type;
impl Trait1 for Type {
    fn trait1(&self) {
        print!("global");
    }
}

{
    use impl Trait1 for Type {
        fn trait1(&self) {
            print!("scoped");
        }
    }

    Type.uses_trait1(); // Works, prints "global".
}

In this case, the implementation of Trait2 is not shadowed at all. Additionally, since self.trait1(); here binds Trait on Type directly, rather than on a bounded generic type parameter, it uses whichever impl Trait1 for Type is in scope where it is written.

Resolution on generic type parameters

Scoped impl Trait for Types (including use-declarations) can be applied to outer generic type parameters at least (see unresolved-questions) via scoped blanket use impl<T: Bound> Trait for T.

However, a blanket implementation can only be bound on a generic type parameter iff its bounds are fully covered by the generic type parameter’s bounds and other available trait implementations on the generic type parameter, in the same way as this applies for global implementations.

Method resolution to scoped implementation without trait in scope

Method calls can bind to scoped implementations even when the declaring trait is not separately imported. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Type;
struct Type2;

mod nested {
    trait Trait {
        fn method(&self) {}
    }
}

use impl nested::Trait for Type {}
impl nested::Trait for Type2 {}

Type.method(); // Compiles.
Type2.method(); // error[E0599]: no method named `method` found for struct `Type2` in the current scope

This also equally (importantly) applies to scoped implementations imported from elsewhere.

Scoped implementations do not implicitly bring the trait into scope

This so that no method calls on other types become ambiguous:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Type;
struct Type2;

mod nested {
    trait Trait {
        fn method(&self) {}
    }

    trait Trait2 {
        fn method(&self) {}
    }
}

use nested::Trait2;
impl Trait2 for Type {}
impl Trait2 for Type2 {}

use impl nested::Trait for Type {}
impl nested::Trait for Type2 {}

Type.method(); // Compiles, binds to scoped implementation of `Trait`.
Type2.method(); // Compiles, binds to global implementation of `Trait2`.

(If Trait was not yet globally implemented for Type2, and Trait and Type2 were defined in other crates, then bringing Trait into scope here could introduce instability towards that implementation later being added in one of those crates.)

Shadowing with different bounds

Scoped implementations may have different bounds compared to an implementation they (partially) shadow. The compiler will attempt to satisfy those bounds, but if they are not satisfied, then the other implementation is not shadowed for that set of generic type parameters and no additional warning or error is raised.

(Warnings for e.g. unused scoped implementations and scoped implementations that only shadow a covering global implementation are still applied as normal. It’s just that partial shadowing with different bounds is likely a common use-case in macros.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct Type1;
struct Type2;

trait Trait1 {
    fn trait1() {
        println!("1");
    }
}
impl<T> Trait1 for T {} // <--

trait Trait2 {
    fn trait2() {
        println!("2");
    }
}
impl Trait2 for Type2 {} // <--

trait Say {
    fn say();
}
impl<T: Trait1> Say for T
where
    T: Trait1, // <--
{
    fn say() {
        T::trait1();
    }
}

{
    use impl<T> Say for T
    where
        T: Trait2 // <--
    {
        fn say() {
            T::trait2();
        }
    }

    Type1::say(); // 1
    Type2::say(); // 2
}

No priority over type-associated methods

Scoped impl Trait for Type has the same method resolution priority as an equivalent global implementation would have if it was visible for method-binding in that scope. This means that directly type-associated functions still bind with higher priority than those available through scoped implementations.

Coercion to trait objects

Due to the coercion into a trait object in the following code, the scoped implementation becomes attached to the value through the pointer meta data. This means it can then be called from other scopes:

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::{self, Display, Formatter};

fn function() -> &'static dyn Display {
    use impl Display for () {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            write!(f, "scoped")
        }
    }

    &()
}

println!("{}", function()); // "scoped"

This behaves exactly as a global implementation would.

Note that the DynMetadata<dyn Display>s of the reference returned above and one that uses the global implementation would compare as distinct even if both are “&()”.

Interaction with return-position impl Trait

Consider the following functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Trait {}

fn function() -> impl Trait {
    use impl Trait for () {}

    () // Binds on trailing `()`-expression.
}

fn function2() -> impl Trait {
    use impl Trait for () {}

    {} // Binds on trailing `{}`-block used as expression.
}

In this case, the returned opaque types use the respective inner scoped implementation, as it binds on the () expression.

The following functions do not compile, as the implicitly returned () is not stated inside the scope where the implementation is available:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
trait Trait {}

fn function() -> impl Trait {
                 ^^^^^^^^^^
    use impl Trait for () {}
    ---------------------

    // Cannot bind on implicit `()` returned by function body without trailing *Expression*.
}

fn function2() -> impl Trait {
                  ^^^^^^^^^^
    use impl Trait for () {}
    ---------------------

    return; // Cannot bind on `return` without expression.
    -------
}

(The errors should ideally also point at the scoped implementations here with a secondary highlight, and suggest stating the return value explicitly.)

The binding must be consistent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Trait {}

fn function() -> impl Trait {
    // error: Inconsistent implementation of opaque return type.
    if true {
        use impl Trait for () {}
        return ();
        ----------
    } else {
        use impl Trait for () {}
        return ();
        ^^^^^^^^^^
    }
}

This function does compile, as the outer scoped impl Trait for () is bound on the if-else-expression as a whole.

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Trait {}

fn function() -> impl Trait {
    use impl Trait for () {}

    if true {
        use impl Trait for () {} // warning: unused scoped implementation
        ()
    } else {
        use impl Trait for () {} // warning: unused scoped implementation
        ()
    }
}

This compiles because the end of the function is not reachable:

1
2
3
4
5
6
7
8
trait Trait {}

fn function() -> impl Trait {
    {
        use impl Trait for () {}
        return (); // Explicit `return` is required to bind in the inner scope.
    }
}

Static interception of dynamic calls

As a consequence of binding outside of generic contexts, it is possible to statically wrap specific trait implementations on concrete types. This includes the inherent implementations on trait objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::fmt::{self, Display, Formatter};

{
    use impl Display for dyn Display {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            // Restore binding to inherent global implementation within this function.
            use ::{impl Display for dyn Display};

            write!(f, "Hello! ")?;
            d.fmt(f)?;
            write!(f, " See you!")
        }
    }

    let question = "What's up?"; // &str
    println!("{question}"); // "What's up?"

    let question: &dyn Display = &question;
    println!("{question}"); // Binds to the scoped implementation; "Hello! What's up? See you!"
}

Warnings

Unused scoped implementation

Scoped implementations and use-declarations of such (including those written as ImplEnvironmentEntry) receive a warning if unused. This can also happen if a use-declaration only reapplies a scoped implementation that is inherited from a surrounding item scope.

(rust-analyzer should suggest removing any unused use-declarations as fix in either case.)

An important counter-example:

Filename: library/src/lib.rs

1
2
3
4
5
6
7
pub struct Type;
pub struct Generic<T>;

pub trait Trait {}
use impl Trait for Type {}

pub type Alias = Generic<Type>;

Filename: main.rs

1
2
3
4
5
use std::any::TypeId;

use library::{Alias, Generic, Type};

assert_ne!(TypeId::of::<Alias>(), TypeId::of::<Generic<Type>>());

Here, the scoped implementation use impl Trait for Type {} is accounted for as it is captured into the type identity of Alias.

Since Alias is exported, the compiler cannot determine within the library alone that the type identity is unobserved. If it can ensure that that is the case, a (different!) warning could in theory still be shown here.

Global trait implementation available

Scoped implementations and use-declarations of such receive a specific warning if only shadowing a global implementation that would fully cover them. This warning also informs about the origin of the global implementation, with a “defined here” marker if in the same workspace. This warning is not applied to scoped implementations that at all shadow another scoped implementation.

(Partial overlap with a shadowed scoped implementation should be enough to suppress this because setting the import up to be a precise subset could get complex fairly quickly. In theory just copying where-clauses is enough, but in practice the amount required could overall scale with the square of scoped implementation shadowing depth and some imports may even have to be duplicated.)

It would make sense to let the definitions and also alternatively specific global implementations of traits with high implementation stability requirements like serde::{Deserialize, Serialize} deactivate this warning too, so that the latter don’t cause it on the respective covered scoped implementations.

Self-referential bound of scoped implementation

1
2
3
4
trait Foo { }

use impl<T> Foo for T where T: Foo { }
            ---------       ^^^^^^

A Rust developer may want to write the above to mean ‘this scoped implementation can only be used on types that already implement this trait’ or ‘this scoped implementation uses functionality of the shadowed implementation’. However, since scoped impl Trait for Type uses item scope rules, any shadowed implementation is functionally absent in the entire scope. As such, this implementation, like the equivalent global implementation, cannot apply to any types at all.

The warning should explain that and why the bound is impossible to satisfy.

Private supertrait implementation required by public implementation

Consider the following code:

1
2
3
4
5
6
7
pub struct Type;

use impl PartialEq for Type {
    // ...
}

pub use impl Eq for Type {}

Here, the public implementation relies strictly on the private implementation to also be available. This means it effectively cannot be imported in use-declarations outside this module.

See also the error incompatible-or-missing-supertrait-implementation.

Public implementation of private trait/on private type

The code

1
2
3
4
5
struct Type;
trait Trait {}

pub use impl Trait for Type {}
             ^^^^^     ^^^^

should produce two distinct warnings similarly to those for private items in public signatures, as the limited visibilities of Type and Trait independently prevent the implementation from being imported in modules for which it is declared as visible.

Scoped implementation is less visible than item/field it is captured in

The code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct Type;
pub struct Generic<U, V>(U, V);

trait Trait {} // <-- Visibility of the trait doesn't matter for *this* warning.

use impl Trait for Type {}
-----------------------

pub type Alias = Generic<Type, Type>;
                         ^^^^  ^^^^

pub fn function(value: Generic<Type, Type>) -> Generic<Type, Type> {
                               ^^^^  ^^^^              ^^^^  ^^^^
    value
}

pub struct Struct {
  private: Generic<Type, Type>, // This is fine.
  pub public: Generic<Type, Type>,
                      ^^^^  ^^^^
}

should produce eight warnings (or four/three warnings with multiple primary spans each, if possible). The warning should explain that the type can’t be referred to by fully specified name outside the crate/module and that the implementation may be callable from code outside the crate/module.

If the binding is specified via inline implementation environment, then the warning should show up on the Trait in module span instead.

Note that as with other private-in-public warnings, replacing

1
use impl Trait for Type {}

with

1
2
3
4
5
mod nested {
    use super::{Trait, Type};
    pub use impl Trait for Type {}
}
use nested::{impl Trait for Type};

in the code sample above should silence the warning.

In some cases, adding as Trait in :: to the generic type argument could be suggested as quick-fix, though generally it’s better to fix this warning by moving the scoped implementation into a nested scope or moving it into a module and importing it into nested scopes as needed.

This warning can’t be suppressed for private traits because the presence of their scoped implementation on a generic type parameter still affects the TypeId of the capturing generic, which here is visible outside of the discretising module.

Imported implementation is less visible than item/field it is captured in

This occurs under the same circumstances as above, except that

1
2
trait Trait {}
use impl Trait for Type {}

is replaced with

1
2
3
4
use a_crate::{
    Trait,
    impl Trait for Type,
};

(where here the implementation import is subsetting a blanket import, but that technicality isn’t relevant. What matters is that the implementation is from another crate).

If the imported implementation is captured in a public item’s signature, that can accidentally create a public dependency. As such this should be a warning too (unless something from that crate occurs explicitly in that public signature or item?).

Errors

Global implementation of trait where global implementation of supertrait is shadowed

A trait cannot be implemented globally for a discrete type in a scope where the global implementation of any of its supertraits is shadowed on that type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Type;

trait Super {}
trait Sub: Super {}

impl Super for Type {}

{
    use impl Super for Type {}
    ----------------------- // <-- Scoped implementation defined/imported here.

    impl Sub for Type {}
    ^^^^^^^^^^^^^^^^^ //<-- error: global implementation of trait where global implementation of supertrait is shadowed
}

Negative scoped implementation

This occurs on all negative scoped implementations. Negative scoped implementations can be parsed, but are rejected shortly after macros are applied.

1
2
3
4
5
6
7
8
9
struct Type;
trait Trait {}

impl Trait for Type {}

{
    use impl !Trait for Type {}
    ^^^^^^^^^^^^^^^^^^^^^^^^ error: negative scoped implementation
}

Incompatible or missing supertrait implementation

Implementations of traits on discrete types require a specific implementation of each of their supertraits, as they bind to them at their definition, so they cannot be used without those being in scope too (to avoid perceived and hard to reason-about inconsistencies).

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Type;
trait Super {}
trait Sub: Super {}

impl Super for Type {}

mod nested {
    pub use impl Super for Type {}
    pub use impl Sub for Type {}
}

use nested::{impl Sub for Type};
             ^^^^^^^^^^^^^^^^^ error: incompatible supertrait implementation

Rustc should suggest to import the required scoped implementation, if possible.

See also the warning private-supertrait-implementation-required-by-public-implementation. See also implicit-import-of-supertrait-implementations-of-scoped-implementations-defined-on-discrete-types for a potential way to improve the ergonomics here.

Scoped implementation of external sealed trait

Given crate a:

1
2
3
4
5
6
7
8
mod private {
    pub trait Sealing {}
}
use private::Sealing;

pub trait Sealed: Sealing {}

pub use impl<T> Sealed for T {} // Ok.

And crate b:

1
2
3
4
5
6
7
use a::{
    Sealed,
    impl Sealed for usize, // Ok.
};

use impl Sealed for () {} // Error.
         ^^^^^^

Crate b cannot define scoped implementations of the external sealed trait Sealed, but can still import them.

See no-external-scoped-implementations-of-sealed-traits for why this is necessary.

Behaviour change/Warning: TypeId of implementation-aware generic discretised using generic type parameters

As a result of the transmutation permission given in layout-compatibility, which is needed to let the ErasedHashSet example in typeid-of-generic-type-parameters-opaque-types remain sound, monomorphisations of a function that observe distinct TypeIds for implementation-aware-generics they discretise using type parameters may be called on the same value instance.

Notably, this affects TypeId::of::<Self>() in implementations with most generic targets, but not in unspecific blanket implementations on the type parameter itself.

This would have to become a future incompatibility lint ahead of time, and should also remain a warning after the feature is implemented since the behaviour of TypeId::of::<Self>() in generics is likely to be unexpected.

In most cases, implementations should change this to TypeId::of::<T>(), where T is the type parameter used for discretisation, since that should show only the expected TypeId distinction.

Instead of TypeId::of::<AStruct<U, V, W>>(), TypeId::of::<(U, V, W)>() can be used, as tuples are implementation-invariant-generics.

Drawbacks

Why should we not do this?

First-party implementation assumptions in macros

If a macro outputs a call of the form <$crate::Type as $crate::Trait>::method(), it can currently make safety-critical assumptions about implementation details of the method that is called iff implemented in the same crate.

(This should also be considered relevant for library/proc-macro crate pairs where the macro crate is considered an implementation detail of the library even where the macro doesn’t require an unsafe token in its input, even though “crate privacy” currently isn’t formally representable towards Cargo.)

As such, newly allowing the global trait implementation to be shadowed here can introduce soundness holes iff Trait is not unsafe or exempt from scoped implementations.

(I couldn’t come up with a good example for this. There might be a slim chance that it’s not actually a practical issue in the ecosystem. Unfortunately, this seems to be very difficult to lint for.)

There are a few ways to mitigate this, but they all have significant drawbacks:

  • Opt-in scoped-impl Trait transparency for macros

    This would make scoped impl Trait for Types much less useful, as they couldn’t be used with for example some derive macros by default. It would also be necessary to teach the opt-in along with macros, which may not be realistic considering existing community-made macro primers.

    Implementation is likely complicated because many procedural macros emit tokens only with Span::call_site() hygiene, so information on the distinct binding site origin may not be readily available.

    This could be limited to existing kinds of macro definitions, so that future revised macro systems can be opted in by default. Future macros could use an unsafe trait instead to assume an implementation, or make use of scoped impl Trait for Type to enforce a specific implementation in their output.

    Drawback: Whether globally implemented behaviour can be changed by the consumer would depend on the macro. It would be good to surface a transparency opt-in in the documentation here.

  • Opt-in scoped-impl Trait priority for macros

    This would preserve practical usefulness of the proposed feature in most cases.

    This would add significant complexity to the feature, as resolution of scoped implementations wouldn’t be exactly the same as for other items. (We should otherwise warn if a scoped impl Trait for Type outside a macro shadows binding a global implementation inside of it though, so at least the feature implementation complexity may be net zero in this regard.)

    This could be limited to existing kinds of macro definitions, with the same implications as for opt-in transparency above.

    Drawback: Whether globally implemented behaviour can be changed by the consumer would depend on the macro. It would be good to surface a priority opt-in in the documentation here.

  • Forbid scoped impl Trait for Type if Trait and Type are from the same crate

    This would at best be a partial fix and would block some interesting uses of using-scoped-implementations-to-implement-external-traits-on-external-types.

Unexpected behaviour of TypeId::of::<Self>() in implementations on generics in the consumer-side presence of scoped implementations and transmute

As explained in rustdoc-documentation-changes, layout-compatibility and type-identity-of-generic-types, an observed TypeId can change for an instance under specific circumstances that are previously-legal transmutes as e.g. for the HashSets inside the type-erased value-keyed collection like the ErasedHashSet example in the typeid-of-generic-type-parameters-opaque-types section.

This use case appears to be niche enough in Rust to not have an obvious example on crates.io, but see behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters for a lint that aims to mitigate issues in this regard and could be used to survey potential issues.

More use-declaration clutter, potential inconsistencies between files

If many scoped implementations need to be imported, this could cause the list of use-declarations to become less readable. If there are multiple alternatives available, inconsistencies could sneak in between modules (especially if scoped impl Trait for Type is used in combination with specialisation).

This can largely be mitigated by centralising a crate’s scoped trait imports and implementations in one module, then wildcard-importing its items:

1
2
3
// lib.rs
mod scoped_impls;
use scoped_impls::*;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// scoped_impls.rs
use std::fmt::Debug;

use a::{TypeA, TraitA};
use b::{TypeB, TraitB};

pub use a_b_glue::{impl TraitA for TypeB, impl TraitB for TypeA};
// ...

pub use impl Debug for TypeA {
    // ...
}
pub use impl Debug for TypeB {
    // ...
}

// ...
1
2
// other .rs files
use crate::scoped_impls::*;

Type inference has to consider both scoped and global implementations

Complexity aside, this could cause compiler performance issues since caching would be less helpful.

Fortunately, at least checking whether scoped implementations exist at all for a given trait and item scope should be reasonably inexpensive, so this hopefully won’t noticeably slow down compilation of existing code.

That implementation environment binding on generic type parameters is centralised to the type discretisation site(s) may also help a little in this regard.

Cost of additional monomorphised implementation instances

The additional instantiations of implementations resulting from binding-choice-by-implementations-bounds could have a detrimental effect on compile times and .text size (depending on optimisations).

This isn’t unusual for anything involving GenericParams, but use of this feature could act as a multiplier to some extent. It’s likely a good idea to evaluate relatively fine-grained caching in this regard, if that isn’t in place already.

Split type identity may be unexpected

Consider crates like inventory or Bevy’s systems and queries.

There may be tricky to debug issues for their consumers if a TypeId doesn’t match between uses of generics with superficially the same type parameters, especially without prior knowledge of distinction by captured implementation environments.

A partial mitigation would be to have rustc include captured scoped implementations on generic type parameters when printing types, but that wouldn’t solve the issue entirely.

Note that with this RFC implemented, TypeId would still report the same value iff evaluated on generic type parameters with distinct but bound-irrelevant captured implementations directly, as long as only these top-level implementations differ and no nested captured implementation environments do.

Marking a generic as implementation-invariant is a breaking change

This concerns the split of implementation-aware-generics and implementation-invariant-generics.

“Implementation-aware” is the logic-safe default.

“Implementation-invariant” has better ergonomics in some cases.

It would be great to make moving from the default here only a feature addition. To do this, a new coherence rule would likely have to be introduced to make implementations conflict if any type becoming implementation-invariant would make them conflict, and additionally to make such implementations shadow each other (to avoid all-too-unexpected silent behaviour changes).

However, even that would not mitigate the behaviour change of type-erasing collections that are keyed on such generics that become type-invariant later, so making this a breaking change is simpler and overall more flexible.

Rationale and alternatives

Avoid newtypes’ pain points

Alternative keywords: ergonomics and compatibility.

Recursively dependent #[derive(…)]

Many derives, like Clone, Debug, partial comparisons, serde::{Deserialize, Serialize} and bevy_reflect::{FromReflect, Reflect} require the trait to be implemented for each field type. Even with the more common third-party traits like Serde’s, there are many crates with useful data structures that do not implement these traits directly.

As such, glue code is necessary.

Current pattern

Some crates go out of their way to provide a compatibility mechanism for their derives, but this is neither the default nor has it (if available) any sort of consistency between crates, which means finding and interacting with these mechanisms requires studying the crate’s documentation in detail.

For derives that do not provide such a mechanism, often only newtypes like NewSerdeCompatible and NewNeitherCompatible below can be used. However, these do not automatically forward all traits (and forwarding implementations may be considerably more painful than the derives), so additional glue code between glue crates may be necessary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use bevy_reflect::Reflect;
use serde::{Deserialize, Serialize};

use bevy_compatible::BevyCompatible;
use neither_compatible::NeitherCompatible;
use serde_compatible::SerdeCompatible;

// I could not actually find much information on how to implement the Bevy-glue.
// I assume it's possible to provide at least this API by creating a newtype and implementing the traits manually.
use bevy_compatible_serde_glue::BevyCompatibleDef;
use neither_compatible_bevy_glue::NewNeitherCompatible; // Assumed to have `From`, `Into` conversions.
use neither_compatible_serde_glue::NeitherCompatibleDef;
use serde_compatible_bevy_glue::NewSerdeCompatible; // Assumed to have `From`, `Into` conversions.

/// A typical data transfer object as it may appear in a service API.
#[derive(Deserialize, Serialize, Reflect)]
#[non_exhaustive] // Just a reminder, since the fields aren't public anyway.
pub struct DataBundle {
    // Serde provides a way to use external implementations on fields (but it has to be specified for each field separately).
    // Bevy does not have such a mechanism so far, so newtypes are required.
    // The newtypes should be an implementation detail, so the fields are (for consistency all) private.
    #[serde(with = "NewSerdeCompatibleDef")]
    serde: NewSerdeCompatible,
    #[serde(with = "BevyCompatibleDef")]
    bevy: BevyCompatible,
    #[serde(with = "NewNeitherCompatibleDef")]
    neither: NewNeitherCompatible,
}

// Some of the newtypes don't implement `Default` (maybe it was added to the underlying types later and the glue crate doesn't want to bump the dependency),
// so this has to be implemented semi-manually instead of using the `derive`-macro.
impl Default for DataBundle {
    fn default() -> Self {
        DataBundleParts::default().into()
    }
}

// If the Bevy glue doesn't forward the Serde implementations, this is necessary.
#[derive(Deserialize, Serialize)]
#[serde(remote = "NewSerdeCompatible")]
#[serde(transparent)]
struct NewSerdeCompatibleDef(SerdeCompatible);

// Same as above, but here the implementation is redirected to another glue crate.
#[derive(Deserialize, Serialize)]
#[serde(remote = "NewNeitherCompatible")]
#[serde(transparent)]
struct NewNeitherCompatibleDef(#[serde(with = "NeitherCompatibleDef")] NeitherCompatible);

impl DataBundle {
    // These conversions are associated functions for discoverability.
    pub fn from_parts(parts: DataBundleParts) -> Self {
        parts.into()
    }
    pub fn into_parts(self) -> DataBundleParts {
        self.into()
    }

    // Necessary to mutate multiple fields at once.
    pub fn parts_mut(&mut self) -> DataBundlePartsMut<'_> {
        DataBundlePartsMut {
            serde: &mut self.serde.0,
            bevy: &mut self.bevy,
            neither: &mut self.neither.0,
        }
    }

    // Accessors to the actual instances with the public types.
    pub fn serde(&self) -> &SerdeCompatible {
        &self.serde.0
    }
    pub fn serde_mut(&mut self) -> &mut SerdeCompatible {
        &mut self.serde.0
    }

    // This also uses an accessor just for consistency.
    pub fn bevy(&self) -> &BevyCompatible {
        &self.bevy
    }
    pub fn bevy_mut(&mut self) -> &mut BevyCompatible {
        &mut self.bevy
    }

    // More accessors.
    pub fn neither(&self) -> &NeitherCompatible {
        &self.neither.0
    }
    pub fn neither_mut(&mut self) -> &mut NeitherCompatible {
        &mut self.neither.0
    }
}

// Conversions for convenience
impl From<DataBundleParts> for DataBundle {
    fn from(value: DataBundleParts) -> Self {
        Self {
            serde: value.serde.into(),
            bevy: value.bevy.into(),
            neither: value.neither.into(),
        }
    }
}

impl From<DataBundle> for DataBundleParts {
    fn from(value: DataBundle) -> Self {
        Self {
            serde: value.serde.into(),
            bevy: value.bevy,
            neither: value.neither.into(),
        }
    }
}

/// Used to construct and destructure [`DataBundle`].
#[derive(Default)] // Assume that all the actual field types have useful defaults.
#[non_exhaustive]
pub struct DataBundleParts {
    pub serde: SerdeCompatible,
    pub bevy: BevyCompatible,
    pub neither: NeitherCompatible,
}

/// Return type of [`DataBundle::parts_mut`].
#[non_exhaustive]
pub struct DataBundlePartsMut<'a> {
    pub serde: &'a mut SerdeCompatible,
    pub bevy: &'a mut BevyCompatible,
    pub neither: &'a mut NeitherCompatible,
}

If two traits that require newtype wrappers need to be added for the same type, the process can be even more painful than what’s shown above, involving unsafe reinterpret casts to borrow a wrapped value correctly as each newtype and forwarding-implementing each trait manually if no transparent derive is available.

With scoped impl Trait for Type

Scoped impl Trait for Type eliminates these issues, in a standardised way that doesn’t require any special consideration from the trait or derive crates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use bevy_reflect::Reflect;
use serde::{Deserialize, Serialize};

use bevy_compatible::BevyCompatible;
use neither_compatible::NeitherCompatible;
use serde_compatible::SerdeCompatible;

// I could not actually find much information on how to implement Bevy-glue.
// It's about the same as manually implementing the traits for newtypes, though.
// Since many traits are required for `bevy_reflect`'s derives, those glue crates use the prelude pattern and provide one for each target type.
use bevy_compatible_serde_glue::{
    impl Deserialize<'_> for BevyCompatible,
    impl Serialize for BevyCompatible,
};
use neither_compatible_bevy_glue::preludes::neither_compatible::*;
use neither_compatible_serde_glue::{
    impl Deserialize<'_> for NeitherCompatible,
    impl Serialize for NeitherCompatible,
};
use serde_compatible_bevy_glue::preludes::serde_compatible::*;

/// A typical data transfer object as it may appear in a service API.
#[derive(Default, Deserialize, Serialize, Reflect)]
#[non_exhaustive]
pub struct DataBundle {
    // Everything just works.
    pub serde: SerdeCompatible,
    pub bevy: BevyCompatible,
    pub neither: NeitherCompatible,
}

// `Default` was derived normally.
// No glue for the glue is necessary.
// No conversions are needed to construct or destructure.
// `&mut`-splitting is provided seamlessly by Rust.
// No accessors are needed since the fields are public.

Even in cases where the glue API cannot be removed, it’s still possible to switch to this simplified, easier to consume implementation and deprecate the original indirect API.

Note that the imported scoped implementations are not visible in the public API here, since they do not appear on generic type parameters in public items. There may still be situations in which defining a type alias is necessary to keep some scoped implementations away from generic type parameters. In some cases, it could be enough to add as Trait in :: to generic type arguments to restore their implementation environment to contain global implementations only.

In some cases, where a field type is quoted in a derive macro directly, writing (Type as Trait in module) only there could in theory also work, but this would heavily depend on the macro’s implementation details. See also should-it-be-an-error-to-specify-an-implementation-environment-in-places-where-its-guaranteed-to-be-unused.

Unlike with external newtypes, there are no potential conflicts beyond overlapping imports and definitions in the same scope. These conflicts can always be resolved both without editing code elsewhere and without adding an additional implementation:

  • either by narrowing a local blanket implementation,
  • by narrowing a blanket implementation import to a subset of the external implementation,
  • or at worst by moving a generic implementation into a submodule and importing it for discrete types.

Error handling and conversions

When implementing services, it’s a common pattern to combine a framework that dictates function signatures with one or more unrelated middlewares that have their own return and error types. The example below is a very abridged example of this.

Note that in either version, the glue code may be project-specific. Glue code is very slightly more concise when implemented with scoped impl Trait for Type, as intermediary struct definitions and the resulting field access can be avoided.

Current pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// crate `service`

use framework::{Error, Returned};
use middleware_a::{fallible_a, Error as ErrorA};
use middleware_b::{fallible_b, Error as ErrorB};

use framework_middleware_a_glue::{IntoReturnedExt as _, NewErrorA};
use framework_middleware_b_glue::{IntoReturnedExt as _, NewErrorB};

pub fn a() -> Result<Returned, Error> {
    // A `try` block should work eventually, but it may be not much less verbose.
    Ok((|| -> Result<_, NewErrorA> {
        fallible_a()?;
        Ok(fallible_a()?)
    })()?
    .into_returned())
}

pub fn b() -> Result<Returned, Error> {
    // The same as above.
    Ok((|| -> Result<_, NewErrorB> {
        fallible_b()?;
        Ok(fallible_b()?)
    })()?
    .into_returned())
}

pub fn mixed(condition: bool) -> Result<Returned, Error> {
    // Neither 'NewError' type provided by third-party crates can be used directly here.
    Ok((move || -> Result<_, NewError> {
        Ok(if condition {
            fallible_b()?;
            fallible_a()?.into_returned()
        } else {
            fallible_a()?;
            fallible_b()?.into_returned()
        })
    })()?)
}

// Custom glue to connect all three errors:
struct NewError(Error);
impl From<NewError> for Error {
    fn from(value: NewError) -> Self {
        value.0
    }
}
impl From<ErrorA> for NewError {
    fn from(value: ErrorA) -> Self {
        let intermediate: NewErrorA = value.into();
        Self(intermediate.into())
    }
}
impl From<ErrorB> for NewError {
    fn from(value: ErrorB) -> Self {
        let intermediate: NewErrorB = value.into();
        Self(intermediate.into())
    }
}
1
2
3
4
5
6
7
8
9
10
use service::{a, b, mixed};

fn main() {
    framework::setup()
        .add_route("a", a)
        .add_route("b", b)
        .add_route("mixed", mixed)
        .build()
        .run();
}

With scoped impl Trait for Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// crate `service`

// More concise, since middleware errors are used only once in imports.
use framework::{Error, Returned};
use middleware_a::fallible_a;
use middleware_b::fallible_b;

// Note: It is often better to import `impl Into` here over `impl From`,
//       since middleware types often don't appear in public signatures.
//
//       If the target type of the import must appear as type parameter in a public signature,
//       a module that is wildcard-imported into each function body can be used instead,
//       which would amount to 6 additional and 2 modified lines here.
//
//       This RFC includes a warning for unintentionally exposed scoped implementations.
use framework_middleware_a_glue::{
    impl Into<Returned> for middleware_a::Returned,
    impl Into<Error> for middleware_a::Error,
};
use framework_middleware_b_glue::{
    impl Into<Returned> for middleware_b::Returned,
    impl Into<Error> for middleware_b::Error,
};

pub fn a() -> Result<Returned, Error> {
    // It just works.
    fallible_a()?;
    Ok(fallible_a()?.into())
}

pub fn b() -> Result<Returned, Error> {
    // Here too.
    fallible_b()?;
    Ok(fallible_b()?.into())
}

pub fn mixed(condition: bool) -> Result<Returned, Error> {
    // This too just works, as conversions bind separately.
    Ok(if condition {
        fallible_b()?;
        fallible_a()?.into()
    } else {
        fallible_a()?;
        fallible_b()?.into()
    })
}

// No custom glue is necessary at all.
1
2
3
4
5
6
7
8
9
10
11
12
// Unchanged. No change in the API of `service`, either.

use service::{a, b, mixed};

fn main() {
    framework::setup()
        .add_route("a", a)
        .add_route("b", b)
        .add_route("mixed", mixed)
        .build()
        .run();
}

Note that to export discrete scoped impl Into in addition to their scoped impl From, the glue crates can use the following pattern, which discretises the global implementation and as such binds to each scoped impl From in the respective exported scoped impl Into:

1
2
3
4
pub use ::{
    impl Into<framework::Returned> for middleware_a::Returned,
    impl Into<framework::Error> for middleware_a::Error,
};

Preserve coherence

Cross-crate stability

With this RFC, scopes are a ‘mini version’ of the environment that global implementations exist in. As this environment is sealed within one scope, and not composed from multiple crates that may update independently, the orphan rule is not necessary.

All other coherence rules and (for exported implementations) rules for what is and is not a breaking change apply within each scope exactly like for global implementations. In particular:

  • Blanket implementations like

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // (Does not compile!)
    
    use std::fmt::{Debug, LowerHex, Pointer};
    mod debug_by_lower_hex;
    
    use debug_by_lower_hex::{impl<T: LowerHex> Debug<T> for T}; // <--
    
    use impl<T: Pointer> Debug<T> for T { // <--
      // ...
    }
    

    still conflict regardless of actual implementations of LowerHex and Pointer because they may overlap later and

  • because scoped implementation are explicitly subset where they are imported, it is not a breaking change to widen an exported scoped implementation.

    (This is part of the reason why scoped impl Trait for Types are anonymous; names would make these imports more verbose rather than shorter, since the subsetting still needs to happen in every case.)

Logical consistency

Binding external top-level implementations to types is equivalent to using their public API in different ways, so no instance-associated consistency is expected here. Rather, values that are used in the same scope behave consistently with regard to that scope’s visible implementations.

of generic collections

Generics are trickier, as their instances often do expect trait implementations on generic type parameters that are consistent between uses but not necessarily declared as bounded on the struct definition itself.

This problem is solved by making the impls available to each type parameter part of the the type identity of the discretised host generic, including a difference in TypeId there as with existing monomorphisation.

(See type-parameters-capture-their-implementation-environment and type-identity-of-generic-types in the reference-level-explanation above for more detailed information.)

Here is an example of how captured implementation environments safely flow across module boundaries, often seamlessly due to type inference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
pub mod a {
    // ⓐ == ◯

    use std::collections::HashSet;

    #[derive(PartialEq, Eq)]
    pub struct A;

    pub type HashSetA = HashSet<A>;
    pub fn aliased(_: HashSetA) {}
    pub fn discrete(_: HashSet<A>) {}
    pub fn generic<T>(_: HashSet<T>) {}
}

pub mod b {
    // ⓑ

    use std::{
        collections::HashSet,
        hash::{Hash, Hasher},
    };

    #[derive(PartialEq, Eq)]
    pub struct B;
    use impl Hash for B {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }

    pub type HashSetB = HashSet<B>; // ⚠
    pub fn aliased(_: HashSetB) {}
    pub fn discrete(_: HashSet<B>) {} // ⚠
    pub fn generic<T>(_: HashSet<T>) {}
}

pub mod c {
    // ⓒ == ◯

    use std::collections::HashSet;

    #[derive(PartialEq, Eq, Hash)]
    pub struct C;

    pub type HashSetC = HashSet<C>;
    pub fn aliased(_: HashSetC) {}
    pub fn discrete(_: HashSet<C>) {}
    pub fn generic<T>(_: HashSet<T>) {}
}

pub mod d {
    // ⓓ

    use std::{
        collections::HashSet,
        hash::{Hash, Hasher},
        iter::once,
    };

    use super::{
        a::{self, A},
        b::{self, B},
        c::{self, C},
    };

    use impl Hash for A {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }
    use impl Hash for B {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }
    use impl Hash for C {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }

    fn call_functions() {
        a::aliased(HashSet::new()); // ⓐ == ◯
        a::discrete(HashSet::new()); // ⓐ == ◯
        a::generic(HashSet::from_iter(once(A))); // ⊙ == ⓓ

        b::aliased(HashSet::from_iter(once(B))); // ⓑ
        b::discrete(HashSet::from_iter(once(B))); // ⓑ
        b::generic(HashSet::from_iter(once(B))); // ⊙ == ⓓ

        c::aliased(HashSet::from_iter(once(C))); // ⓒ == ◯
        c::discrete(HashSet::from_iter(once(C))); // ⓒ == ◯
        c::generic(HashSet::from_iter(once(C))); // ⊙ == ⓓ
    }
}

Note that the lines annotated with // ⚠ produce a warning due to the lower visibility of the scoped implementation in b.

Circles denote implementation environments:

  
indistinct from global
ⓐ, ⓑ, ⓒ, ⓓrespectively as in module a, b, c, d
caller-side

The calls infer discrete HashSets with different Hash implementations as follows:

call in call_functionsimpl Hash incaptured in/atnotes
a::aliased-type aliasThe implementation cannot be ‘inserted’ into an already-specified type parameter, even if it is missing.
a::discrete-fn signatureSee a::aliased.
a::genericdonce<T> call 
b::aliasedbtype alias 
b::discretebfn signature 
b::genericdonce<T> callb’s narrow implementation cannot bind to the opaque T.
c::aliased::type aliasSince the global implementation is visible in c.
c::discrete::fn signatureSee c::aliased.
c::genericdonce<T> callThe narrow global implementation cannot bind to the opaque T.

of type-erased collections

Type-erased collections such as the ErasedHashSet shown in typeid-of-generic-type-parameters-opaque-types require slightly looser behaviour, as they are expected to mix instances between environments where only irrelevant implementations differ (since they don’t prevent this mixing statically like std::collections::HashSet, as their generic type parameters are transient on their methods).

It is for this reason that the TypeId of generic type parameters disregards bounds-irrelevant implementations.

The example is similar to the previous one, but aliased has been removed since it continues to behave the same as discrete. A new set of functions bounded is added:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#![allow(unused_must_use)] // For the `TypeId::…` lines.

trait Trait {}

pub mod a {
    // ⓐ == ◯

    use std::{collections::HashSet, hash::Hash};

    #[derive(PartialEq, Eq)]
    pub struct A;

    pub fn discrete(_: HashSet<A>) {
        TypeId::of::<HashSet<A>>(); // ❶
        TypeId::of::<A>(); // ❷
    }
    pub fn generic<T: 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
    pub fn bounded<T: Hash + 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
}

pub mod b {
    // ⓑ

    use std::{
        collections::HashSet,
        hash::{Hash, Hasher},
    };

    use super::Trait;

    #[derive(PartialEq, Eq)]
    pub struct B;
    use impl Hash for B {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }
    use impl Trait for B {}

    pub fn discrete(_: HashSet<B>) { // ⚠⚠
        TypeId::of::<HashSet<B>>(); // ❶
        TypeId::of::<B>(); // ❷
    }
    pub fn generic<T: 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
    pub fn bounded<T: Hash + 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
}

pub mod c {
    // ⓒ == ◯

    use std::{collections::HashSet, hash::Hash};

    use super::Trait;

    #[derive(PartialEq, Eq, Hash)]
    pub struct C;
    impl Trait for C {}

    pub fn discrete(_: HashSet<C>) {
        TypeId::of::<HashSet<C>>(); // ❶
        TypeId::of::<C>(); // ❷
    }
    pub fn generic<T: 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
    pub fn bounded<T: Hash + 'static>(_: HashSet<T>) {
        TypeId::of::<HashSet<T>>(); // ❶
        TypeId::of::<T>(); // ❷
    }
}

pub mod d {
    // ⓓ

    use std::{
        collections::HashSet,
        hash::{Hash, Hasher},
        iter::once,
    };

    use super::{
        a::{self, A},
        b::{self, B},
        c::{self, C},
        Trait,
    };

    use impl Hash for A {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }
    use impl Hash for B {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }
    use impl Hash for C {
        fn hash<H: Hasher>(&self, _state: &mut H) {}
    }

    use impl Trait for A {}
    use impl Trait for B {}
    use impl Trait for C {}

    fn call_functions() {
        a::discrete(HashSet::new()); // ⓐ == ◯
        a::generic(HashSet::from_iter(once(A))); // ⊙ == ⓓ
        a::bounded(HashSet::from_iter(once(A))); // ⊙ == ⓓ

        b::discrete(HashSet::from_iter(once(B))); // ⓑ
        b::generic(HashSet::from_iter(once(B))); // ⊙ == ⓓ
        b::bounded(HashSet::from_iter(once(B))); // ⊙ == ⓓ

        c::discrete(HashSet::from_iter(once(C))); // ⓒ == ◯
        c::generic(HashSet::from_iter(once(C))); // ⊙ == ⓓ
        c::bounded(HashSet::from_iter(once(C))); // ⊙ == ⓓ
    }
}

// ⚠ and non-digit circles have the same meanings as above.

The following table describes how the types are observed at runtime in the lines marked with ❶ and ❷. Types are denoted as if seen from the global implementation environment with differences written inline, which should resemble how they are formatted in compiler messages and tooling.

within function
(called by call_functions)
❶ (collection)❷ (item)
a::discreteHashSet<A>A
a::genericHashSet<A as Hash in d + Trait in d>A
a::boundedHashSet<A as Hash in d + Trait in d>AHash in d
b::discreteHashSet<B as Hash in b + Trait in b>B
b::genericHashSet<B as Hash in d + Trait in d>B
b::boundedHashSet<B as Hash in d + Trait in d>BHash in d
c::discreteHashSet<C>C
c::genericHashSet<C as Hash in d + Trait in d>C
c::boundedHashSet<C as Hash in d + Trait in d>CHash in d

The combination ∘ is not directly expressible in TypeId::of::<> calls (as even a direct top-level annotation would be ignored without bounds). Rather, it represents an observation like this:

1
2
3
4
5
6
7
8
9
10
11
12
{
    use std::{any::TypeId, hash::Hash};

    use a::A;
    use d::{impl Hash for A};

    fn observe<T: Hash + 'static>() {
        TypeId::of::<T>(); // '`A` ∘ `Hash in d`'
    }

    observe::<A>();
}
with multiple erased type parameters

By replacing the lines

1
2
TypeId::of::<HashSet<T>>(); // ❶
TypeId::of::<T>(); // ❷

with

1
2
TypeId::of::<HashSet<(T,)>>(); // ❶
TypeId::of::<(T)>(); // ❷

(and analogous inside the discrete functions), the TypeId table above changes as follows:

within function
(called by call_functions)
❶ (collection)❷ (item)
a::discreteHashSet<(A,)>(A,)
a::genericHashSet<(A as Hash in d + Trait in d,)>(A,)
a::boundedHashSet<(A as Hash in d + Trait in d,)>(AHash in d,)
b::discreteHashSet<(B as Hash in b + Trait in b,)>(B,)
b::genericHashSet<(B as Hash in d + Trait in d,)>(B,)
b::boundedHashSet<(B as Hash in d + Trait in d,)>(BHash in d,)
c::discreteHashSet<(C,)>(C,)
c::genericHashSet<(C as Hash in d + Trait in d,)>(C,)
c::boundedHashSet<(C as Hash in d + Trait in d,)>(CHash in d,)

As you can see, the type identity of the tuples appears distinct when contributing to an implementation-aware generic’s type identity but (along with the TypeId) remains appropriately fuzzy when used alone.

This scales up to any number of type parameters used in implementation-invariant generics, which means an efficient ErasedHashMap<S: BuildHasher> can be constructed by keying storage on the TypeId::of::<(K, V)>() where K: Hash + Eq and V are the generic type parameters of its functions.

Logical stability

  • Non-breaking changes to external crates cannot change the meaning of the program.
  • Breaking changes should result in compile-time errors rather than a behaviour change.

This is another consequence of subsetting rather than named-model imports, as narrowing a scoped implementation can only make the use-declaration fail to compile, rather than changing which implementations are shadowed.

Similarly, types of generics with different captured implementation environments are strictly distinct from each other, so that assigning them inconsistently does not compile. This is weighed somewhat against ease of refactoring, so in cases where a type parameter is inferred and the host is used in isolation, which are assumed to not care about implementation details like that, the code will continue to align with the definition instead of breaking.

Encourage readable code

This RFC aims to further decrease the mental workload required for code review, by standardising glue code APIs to some degree and by clarifying their use in other modules.

It also aims to create an import grammar that can be understood more intuitively than external newtypes when first encountered, which should improve the accessibility of Rust code somewhat.

Clear imports

As scoped implementations bind implicitly like global ones, two aspects must be immediately clear at a glace:

  • Which trait is implemented?
  • Which type is targeted?

Restating this information in the use-declaration means that it is available without leaving the current file, in plaintext without any tooling assists. This is another improvement compared to newtypes or external definitions, where the relationship may not be immediately clear depending on their names.

Spelling scoped implementation imports out with keywords rather than just symbols makes their purpose easy to guess for someone unfamiliar with the scoped impl Trait for Type feature, possibly even for most English-speaking developers unfamiliar with Rust.

This is also true for blanket imports with where, which remain easy to parse visually due to the surrounding braces:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt::{Debug, Display, Pointer};

// `Debug` and `Display` all `Pointer`-likes as addresses.
// The `Display` import is different only to show the long form
// with `where`. It could be written like the `Debug` import.
use cross_formatting::by_pointer::{
    impl<T: Pointer> Debug for T,
    {impl<T> Display for T where T: Pointer},
};

println!("{:?}", &()); // For example: 0x7ff75584c360
println!("{}", &()); // For example: 0x7ff75584c360

Familiar grammar

The grammar for scoped implementations differs from that for global implementations by only a prefixed use and an optional visibility. As such, it should be easy to parse for developers not yet familiar with scoped implementations specifically.

The clear prefix (starting with at least two keywords instead of one) should still be enough to distinguish scoped implementations at a glance from global ones.

The header (the part before the {} block) of global implementations is reused unchanged for scoped implementation imports, including all bounds specifications, so there is very little grammar to remember additionally in order to use scoped impl Trait for Types.

In each case, the meaning of identical grammar elements lines up exactly - only their context and perspective vary due to immediately surrounding tokens.

(See grammar-changes for details.)

Stop tokens for humans

When looking for the scoped implementation affecting a certain type, strict shadowing ensures that it is always the closest matching one that is effective.

As such, readers can stop scanning once they encounter a match (or module boundary, whether surrounding or nested), instead of checking the entire file’s length for another implementation that may be present in the outermost scope.

Aside from implementation environments captured inside generics, scoped implementations cannot influence the behaviour of another file without being mentioned explicitly.

Unblock ecosystem evolution

As any number of scoped glue implementations can be applied directly to application code without additional compatibility shims, it becomes far easier to upgrade individual dependencies to their next major version. Compatibility with multiple versions of crates like Serde and bevy_reflect can be provided in parallel through officially supported glue crates.

Additionally, scoped implementations are actually more robust than newtypes regarding certain breaking changes:

A newtype that implements multiple traits could eventually gain a global blanket implementation of one of its traits for types that implement another of its traits, causing a conflict during the upgrade.

In the presence of an overlapping scoped impl Trait for Type, the new blanket implementation is just unambiguously shadowed where it would conflict, which means no change is necessary to preserve the code’s behaviour. A global-trait-implementation-available warning is still shown where applicable to alert maintainers of new options they have.

(See also glue-crate-suggestions for possible future tooling related to this pattern.)

Side-effect: Parallelise build plans (somewhat) more

Serde often takes a long time to build even without its macros. If another complex crate depends on it just to support its traits, this can significantly stretch the overall build time.

If glue code for ‘overlay’ features like Serde traits is provided in a separate crate, that incidentally helps to reduce that effect somewhat:

Since the glue forms a second dependency chain that normally only rejoins in application code, the often heavier core functionality of libraries can build in parallel to Serde and/or earlier glue. Since the glue chain is likely to be less code, it matters less for overall build time whether it has to wait for one or two large crates first.

Provide opportunities for rich tooling

Discovery of implementations

As scoped implementations clearly declare the link between the trait and type(s) they connect, tools like rust-analyzer are able to index them and suggest imports where needed, just like for global traits.

(At least when importing from another crate, the suggested import should be for a specific type or generic, even if the export in question is a blanket implementation. Other generics of the export can usually be preserved, though.)

Discovery of the feature itself

In some cases (where a trait implementations cannot be found at all), tools can suggest creating a scoped implementation, unless adding it in that place would capture it as part of the implementation environment of a type parameter specified in an item definition visible outside the current crate.

That said, it would be great if rust-analyzer could detect and suggest/enable feature-gated global implementations to some extent, with higher priority than creating a new scoped implementation.

Rich and familiar warnings and error messages

Since scoped implementations work much like global ones, many of the existing errors and warnings can be reused with at most small changes. This means that, as developers become more familiar with either category of trait-related issues, they learn how to fix them for global and scoped implementations at the same time.

The implementation of the errors and warnings in the compiler can also benefit from the existing work done for global implementations, or in some cases outright apply the same warning to both scoped and global implementations.

Since available-but-not-imported scoped implementations are easily discoverable by the compiler, they can be used to improve existing errors like error[E0277]: the trait bound […] is not satisfied and error[E0599]: no method named […] found for struct […] in the current scope with quick-fix suggestions also for using an existing scoped implementation in at least some cases.

Maintenance warnings for ecosystem evolution

Scoped impl Trait for Types lead to better maintenance lints:

If a covering global implementation later becomes available through a dependency, a warning can be shown on the local trait implementation for review. (See global-trait-implementation-available.)

In the long run, this can lead to less near-duplicated functionality in the dependency graph, which can lead to smaller executable sizes.

Automatic documentation

Scoped implementations can be documented and appear as separate item category in rustdoc-generated pages.

Rustdoc should be able to detect and annotate captured scoped implementations in public signatures automatically. This, in addition to warnings, could be another tool to help avoid accidental exposure of scoped implementations.

Implementation origin and documentation could be surfaced by rust-analyzer in relevant places.

Why specific implementation-invariant-generics?

This is a not entirely clean ergonomics/stability trade-off, as well as a clean resolution path for behaviour-changewarning-typeid-of-implementation-aware-generic-discretised-using-generic-type-parameters.

It is also the roughest part of this proposal, in my eyes. If you have a better way of dealing with the aware/invariant distinction, please do suggest it!

The main issue is that generics in the Rust ecosystem do not declare which trait implementations on their type parameters need to be consistent during their instances’ lifetime, if any, and that traits like PartialOrd that do provide logical consistency guarantees over time are not marked as such in a compiler-readable way.

Ignoring this and not having distinction of implementation-aware-generics’ discretised variants would badly break logical consistency of generic collections like BTreeSet<T>, which relies on Ord-consistency to function.

On the other hand, certain types (e.g. references and (smart) pointers) that often wrap values in transit between modules really don’t care about implementation consistency on these types. If these were distinct depending on available implementations on their values, it would create considerable friction while defining public APIs in the same scope as struct or enum definitions that require scoped implementations for derives.

Drawing a line manually here is an attempt to un-break this by default for the most common cases while maintaining full compatibility with existing code and keeping awareness of scoped impl Trait for Type entirely optional for writing correct and user-friendly APIs.

As a concrete example, this ensures that Box<dyn Future<Output = Result<(), Error>>> is automatically interchangeable even if spelled out in the presence of scoped error-handling-and-conversions affecting Error, but that BinaryHeap<Box<u8>> and BinaryHeap<Box<u8 as PartialOrd in reverse + Ord in reverse>> don’t mix.

Functions pointers and closure trait( object)s should probably be fairly easy to pass around, with their internally-used bindings being an implementation detail. Fortunately, the Rust ecosystem already uses more specific traits for most configuration for better logical safety, so it’s likely not too messy to make these implementation-invariant.

Traits and trait objects cannot be implementation invariant by default (including for their associated types!) because it’s already possible to define OrderedExtend and OrderedIterator traits with logical consistency requirement on Ord between them.

Efficient compilation

In theory, it should be possible to unify many instances of generic functions that may be polymorphic under this proposal cheaply before code generation. (Very few previously discrete implementations become polymorphic under scoped impl Trait for Type.)

This is mainly an effect of layout-compatibility and binding-choice-by-implementations-bounds, so that, where the differences are only bounds-irrelevant, generated implementations are easily identical in almost all cases. The exception here are implementation-aware-genericsTypeIds (see also typeid-of-generic-type-parameters-opaque-types). Checking for this exception should be cheap if done alongside checks for e.g. function non-constness if possible, which propagates identically from callee to caller.

Given equal usage, compiling code that uses scoped implementations could as such be slightly more efficient compared to use of newtypes and the resulting text size may be slightly smaller in some cases where newtype implementations are inlined differently.

The compiler should treat implementations of the same empty trait on the same type as identical early on, so that no code generation is unnecessarily duplicated. However, unrelated empty-trait implementations must still result in distinct TypeIds when captured in a generic type parameter and observed there by a where-clause or through nesting in an implementation-aware generic.

Alternatives

Named implementations

Use of named implementations is not as obvious as stating the origin-trait-type triple in close proximity, so code that uses named implementations tends to be harder to read.

Like named implementations, the scope-identified implementations proposed here can be written concisely in generic parameter lists (as Type as Trait in module), limiting the code-writing convenience advantage of named implementations. Where needed, the module name can be chosen to describe specific function, e.g. exporting reverse-ordering Ord and PartialOrd implementations from a module called reverse.

If named implementations can’t be brought into scope (see Genus in lightweight-flexible-object-oriented-generics), that limits their practical application to where they can be captured in implementation-aware-generics. Bringing named implementations into scope would be more verbose than for module-trait-type-identified as subsetting would still be required to preserve useful room for library crate evolution.

Weakening coherence rules

There is likely still some leeway here before the Rust ecosystem becomes brittle, but at least the orphan rule specifically is essential for ensuring that global trait implementations do not lead to hard ecosystem splits due to strictly incompatible framework crates.

If other coherence rules are relaxed, scoped impl Trait for Type also benefits immediately since it is subject to all of them.

Crate-private implementations as distinct feature

There is a previous RFC: Hidden trait implementations from 2018-2021 where the result was general acceptance, but postponement for logistical reasons.

Scoped impl Trait for Type together with its warnings scoped-implementation-is-less-visible-than-itemfield-it-is-captured-in and imported-implementation-is-less-visible-than-itemfield-it-is-captured-in can mostly cover this use-case, though with slightly more boilerplate (use-declarations) and not as-strict a limitation.

Required-explicit binding of scoped implementations inside generics

This could avoid the distinction between implementation-aware-generics and implementation-invariant-generics to some extent, at the cost of likely overall worse ergonomics when working with scoped implementations.

It’s also likely to make derive-compatibility of scoped implementations inconsistent, because some macros may require explicit binding on field types while others would not.

Prior art

Lightweight, Flexible Object-Oriented Generics

Yizhou Zhang, Matthew Loring, Guido Salvaneschi, Barbara Liskov and Andrew C. Myers, May 2015

https://www.cs.cornell.edu/andru/papers/genus/

There are some parallels between Genus’s models and the scoped impl Trait for Types proposed in this RFC, but for the most part they are quite distinct due to Rust’s existing features:

Genusscoped impl Trait for Typereasoning
Proper-named modelsAnonymous scoped implementationsUse of existing coherence constraints for validation. Forced subsetting in use-declarations improves stability. The impl Trait for Type syntax stands out in use-declarations and is intuitively readable.
Explicit bindings of non-default modelsMainly implicit bindings, but explicit bindings of scoped and global implementations are possible in some places.Focus on simplicity and ergonomics of the most common use-case. More natural use with future specialisation.
Comparing containers inherently constrain type parameters in their type definition.Available scoped implementations for discretised type parameters become part of the type identity.<p>This is a tradeoff towards integration with Rust’s ecosystem, as generics are generally not inherently bounded on collection types in Rust.</p><p>There is likely some friction here with APIs that make use of runtime type identity. See split-type-identity-may-be-unexpected.</p>

Some features are largely equivalent:

GenusRust (without scoped impl Trait for Type)notes / scoped impl Trait for Type
Implicitly created default modelsExplicit global trait implementationsDuck-typed implementation of unknown external traits is unnecessary since third party crates’ implementations are as conveniently usable in scope as if global.
Runtime model information / Wildcard modelsTrait objectsScoped implementations can be captured in trait objects, and the TypeId of generic type parameters can be examined. This does not allow for invisible runtime specialisation in all cases.
Bindings [only for inherent constraints on generic type parameters?] are part of type identitynot applicable<p>Available implementations on type parameters of discretised implementation-aware generics are part of the type identity. Top-level bindings are not.</p><p>Genus’s approach provides better remote-access ergonomics than 𝒢’s and great robustness when moving instances through complex code, so it should be available. Fortunately, the existing style of generic implementations in Rust can simply be monomorphised accordingly, and existing reflexive blanket conversions and comparisons can bind regardless of unrelated parts of the top-level implementation environment of their type parameters.</p><p>However, typical Rust code also very heavily uses generics like references and closures to represent values passed through crate boundaries. To keep friction acceptably low by default, specific utility types are exempt from capturing implementation environments in their type parameters.</p>

A Language for Generic Programming in the Large

Jeremy G. Siek, Andrew Lumsdaine, 2007

https://arxiv.org/abs/0708.2255

𝒢 and scoped impl Trait for Type are conceptually very similar, though this RFC additionally solves logical consistency issues that arise from having multiple alternative ways to fulfill a constraint and develops some ideas further than the paper. Other differences are largely due to 𝒢 being more C++-like while scoped impl Trait for Type attempts smooth integration with all relevant Rust language features.

A few notable similarities, in the paper’s words:

  • equivalent retroactive modeling (where existing Rust’s is limited by orphan rules),
  • (retained) separate compilation (though some information can flow between items in this RFC, but only where such information flows already exist in Rust currently),
  • lexically scoped models,
  • seemingly the same binding rules on generic type parameters within constrained models/generic implementations,

and key differences:

𝒢Rust / scoped impl Trait for Typenotes
Only discrete model importsIncludes generic imports and re-exportsThis is pointed out as ‘[left] for future work’ in the paper. Here, it follows directly from the syntax combination of Rust’s use and impl Trait for Type items.
-(Rust) Global implementationsThe automatic availability of global implementations between separately imported traits and types offers more convenience especially when working with common traits, like those backing operators in Rust.
Model overloading, mixed into nested scopesStrict shadowingStrict shadowing is easier to reason about for developers (especially when writing macros!), as the search stops at the nearest matching implementation or module boundary.
See Rust’s trait method resolution behaviour and interaction-with-specialisation for how this is still practically compatible with a form of overload resolution.
See scoped-fallback-implementations for a possible future way to better enable adaptive behaviour in macro output.
-(Rust) Trait objects𝒢 does not appear to support runtime polymorphism beyond function pointers. Scoped impl Trait for Type is seamlessly compatible with dyn Trait coercions (iff Trait is object-safe).
(unclear?)Available implementations on discretised type parameters become part of the type identity of implementation-aware generics.This allows code elsewhere to access scoped implementations that are already available at the definition site, and leads to overall more semantically consistent behaviour.

Unresolved questions

“global” implementations

I’m not too sure about the “global” wording. Technically that implementation isn’t available for method calls unless the trait is in scope… though it is available when resolving generics. Maybe “unscoped” is better?

Precise resolution location of implementation environments in function calls

In macros, which function-call token should provide the resolution context from where to look for scoped impl Trait for Types (in all possible cases)?

This doesn’t matter for Span::call_site() vs. Span::mixed_site() since scoped implementations would resolve transparently through both, but it does matter for Span::def_site() which should exclude them.

It very much does matter if one of the opt-in mitigations for first-party-implementation-assumptions-in-macros is implemented.

Which structs should be implementation-invariant?

This is a tough question because, runtime behaviour difference of of-type-erased-collections aside, the following makes shifting a type from implementation-aware-generics to implementation-invariant-generics a compilation-breaking change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Type;
struct Generic<T>(T);
trait Trait {}

mod a {
    use super::{Type, Generic, Trait};
    pub use impl Trait for Type {}
    pub type Alias = Generic<T>;
}

mod b {
    use super::{Type, Generic, Trait};
    pub use impl Trait for Type {}
    pub type Alias = Generic<T>;
}

use impl Trait for a::Alias {}
use impl Trait for b::Alias {}

(It is theoretically possible to do such a later adjustment as part of an edition, even considering TypeId behaviour I think, but it’s certainly not pretty.)

Splitting this along the line of “structs that use <> around type parameters” would feel cleaner, but the basic smart pointers, Pin<P>, Option<T> and Result<T, E> appear in crate API signatures enough that not including them would create considerable friction.

Other candidates for consideration:

  • Other DispatchFromDyn types in the standard library like Cell, SyncUnsafeCell, UnsafeCell

Should it be an error to specify an implementation environment in places where it’s guaranteed to be unused?

With the given grammar-changes, it’s possible to write fn((Type as Trait in module)), but, at least without a surrounding host, here the implementation environment written inline is completely ineffective because function pointer types are discretised implementation-invariant-generics.

On the other hand, making it an error rather than a unused-scoped-implementation warning could easily cause problems for macros.

Future possibilities

Exporting a scoped implementation as global, extern impl Trait

This should never be used for IO/serialisation traits.

Application crates may want to provide a specific implementation globally, disregarding orphan rules since there are no downstream crates that could be impacted by future incompatibilities (and crate-local issues are largely mitigated by Cargo.lock).

This could later be allowed using a construct like

1
2
3
4
5
6
7
// Use an external implementation as global:
#[core::unstable_use_as_global]
use impl_crate::{impl Trait for Type};

// Provide a local implementation globally:
#[core::unstable_use_as_global]
use impl Trait for Type { /*...*/ }

To use a global implementation not available through one of its dependencies, a library crate would have to declare it:

1
extern impl Trait for Type;

This would result in a compile error or link-time error if the declaration is not fully covered by a global trait implementation.

If the trait implementation is later made available plainly (that is: without use, subject to orphan rules) by a dependency, a warning should appear on the extern impl declaration, along with the suggestion to remove the extern impl item.

(However, I assume binding to implementations not-from dependencies or the same crate in this way has a lot of implications for code generation.)

There is previous discussion regarding a similar suggestion in a slightly different context: [Pre-RFC] Forward impls
Perhaps the downsides here could be mitigated by allowing #[unstable_use_as_global] very strictly only in application crates compiled with the cargo --locked flag.

Scoped impl Trait for Type of auto traits, Drop and/or Copy with orphan rules

The crate in which a type is defined could in theory safely provide scoped implementations for it also for these traits.

  • This is likely more complicated to implement than the scoped impl Trait for Types proposed in this RFC, as these traits interact with more distinct systems.

  • What would be the binding site of Drop in let-statements?

  • This could interact with linear types, were those to be added later on.

    For example, database transactions could be opt-out linear by being !Drop globally but also having their crate provide a scoped Drop implementation that can be imported optionally to remove this restriction in a particular consumer scope.

Scoped proxy implementations

In theory it might be possible to later add syntax to create an exported implementation that’s not in scope for itself.

I’m very hesitant about this since doing so would allow transparent overrides of traits (i.e. proxying), which could be abused for JavaScript-style layered overrides through copy-pasting source code together to some extent.

Analogous scoped impl Type

This could be considered as more-robust alternative to non-object-safe extension traits defined in third party crates.

A good example of this use case could be the tap crate, which provides generic extension methods applicable to all types, but where its use is theoretically vulnerable to instability regarding the addition of type-associated methods of the same name(s).

If instead of (or in addition to!) …:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// pipe.rs

pub trait Pipe {
    #[inline(always)]
    fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> R
    where
        Self: Sized,
        R: Sized,
    {
        func(self)
    }

    // ...
}

impl<T> Pipe for T where T: ?Sized {}

…the extension could be defined as …:

1
2
3
4
5
6
7
8
9
10
11
12
pub use impl<T> T where T: ?Sized {
  #[inline(always)]
  fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> R
  where
      Self: Sized,
      R: Sized,
  {
      func(self)
  }

  // ...
}

…then:

  • The consumer crate could choose which types to import the extension for, weighing

    1
    
    use tap::pipe::{impl Type1, impl Type2};
    

    against

    1
    
    use tap::pipe::{impl<T> T where T: ?Sized};
    
  • These scoped extensions would shadow inherent type-associated items of the same name, guaranteeing stability towards those being added.

    (This should come with some warning labels in the documentation for this feature, since adding items to an existing public scoped extension could be considered an easily-breaking change here.)

This has fewer benefits compared to scoped impl Trait for Type, but would still allow the use of such third-party extension APIs in library crates with very high stability requirements.

An open question here is whether (and how) to allow partially overlapping use impl Type in the same scope, in order to not shadow inherent associated items with ones that cannot be implemented for the given type.

  • That could in theory be more convenient to use, but

  • calls could be subtly inconsistent at the consumer side, i.e. accidentally calling an inherent method if a scoped extension method was expected and

  • widening a public implementation to overlap more of another exported in the same module could break dependent crates if a wide blanket import applied to narrower extensions.

As such, if this feature was proposed and accepted at some point in the future, it would likely be a good idea to only allow non-overlapping implementations to be exported.

Interaction with specialisation

  • Scoped impl Trait for Type can be used for consumer-side specialisation of traits for binding sites that are in item scope, by partially shadowing an outer scope’s implementation.

    Note that this would not work on generic type parameters, as the selected implementation is controlled strictly by their bounds (See resolution-on-generic-type-parameters.), but it would work in macros for the most part.

    This does not interact with specialisation proper, but rather is a distinct, less powerful mechanism. As such, it would not supersede specialisation.

  • Scoped impl Trait for Type does not significantly interact with specialisation of global implementations.

    Any global specialisation would only be resolved once it’s clear no scoped implementation applies.

  • Specialisation could disambiguate scoped implementations which are provided (implemented or imported) in the same scope. For example,

    1
    2
    3
    4
    5
    
    use dummy_debug::{impl<T> Debug for T};
    use debug_by_display::{impl<T: Display> Debug for T};
    use impl Debug for str {
        // ...
    }
    

    would then compile, in scope resolving <str as Debug> to the local implementation and otherwise binding Debug depending on whether Display is available at the binding site for each given type T.

    Local implementations do not necessarily have to be more specific compared to imported ones - in keeping with “this is the same as for global implementations”, the way in which the scoped implementation is introduced to the scope should not matter to specialisation.

    When importing scoped implementations from a module, specialisation should apply hierarchically. First, the specificity of implementations is determined only by use impl implementations and use-declarations in the importing scope. If the trait bound binds to a use-declaration, then the actual implementation is chosen by specificity among those visible in the module they are imported from. If the chosen implementation there is an import, the process repeats for the next module. This ensures stability and coherence when published implementations are specialised in other modules.

    • I’m not sure how well this can be cached in the compiler for binding-sites in distinct scopes, unfortunately. Fortunately, specialisation of scoped impl Trait for Type does not seem like a blocker for specialisation of global trait implementations.

    • Should specialisation of scoped implementations require equal visibility? I think so, but this question also seems considerably out of scope for scoped impl Trait as Type as a feature itself.

Scoped impl Trait for Type as associated item

Scoped impl Trait for Type could be allowed and used as associated non-object-safe item as follows:

1
2
3
4
5
6
7
8
trait OuterTrait {
    use impl Trait for Type;
}

fn function<T: OuterTrait>() {
    use T::{impl Trait for Type};
    // ...configured code...
}
1
2
3
4
5
6
7
8
9
impl OuterTrait for OtherType {
    // Or via `use`-declaration of scoped implementation(s) defined elsewhere!
    // Or specify that the global implementation is used (somehow)!
    use impl Trait for Type {
        // ...
    }
}

function::<OtherType>();

This would exactly supersede the following more verbose pattern enabled by this RFC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait OuterTrait {
    type Impl: ImplTraitFor<Type>;
}

trait ImplTraitFor<T: ?Sized> {
    // Copy of trait's associated items, but using `T` instead of the `Self` type and
    // e.g. a parameter named `this` in place of `self`-parameters.
}

fn function<T: OuterTrait>() {
    use impl Trait for Type {
        // Implement using `T::Impl`, associated item by associated item.
    }

    // ...configured code...
}
1
2
3
4
5
6
7
8
9
10
struct ImplTraitForType;
impl ImplTraitFor<Type> for ImplTraitForType {
    // Implement item-by-item, as existing scoped `impl Trait for Type` cannot be used here.
}

impl OuterTrait for OtherType {
    type Impl: ImplTraitFor<Type> = ImplTraitForType;
}

function::<OtherType>();
  • In theory this could be made object-safe if the associated implementation belongs to an object-safe trait, but this would introduce much-more-implicit call indirection into Rust.

Scoped fallback implementations

A scoped fallback implementation could be allowed, for example by negatively bounding it on the same trait in the definition or import:

1
2
3
4
5
6
7
8
9
10
11
#[derive(Debug)]
struct Type1;

struct Type2;

{
    use debug_fallback::{impl<T> Debug for T where T: !Debug};

    dbg!(Type1); // Compiles, uses global implementation.
    dbg!(Type2); // Compiles, uses fallback implementation.
}

This would be a considerably less messy alternative to autoref- or autoderef-specialisation for macro authors.

Note that ideally, these fallback implementations would still be required to not potentially overlap with any other (plain or fallback) scoped implementation brought into that same scope.

Negative scoped implementations

It’s technically possible to allow negative scoped implementations that only shadow the respective implementation from an outer scope. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// signed-indexing/src/arrays/prelude.rs
use core::ops::Index;

pub use impl<T, const N: usize> !Index<usize> for [T; N] {}
pub use impl<T, const N: usize> Index<isize> for [T; N] {
    type Output = T;

    #[inline]
    #[track_caller]
    fn index(&self, index: isize) -> &T {
        match index {
            0.. => self[index as usize],
            ..=-1 => if let Some(index) = self.len().checked_add_signed(index) {
                self[index]
            } else {
                #[inline(never)]
                #[track_caller]
                fn out_of_bounds(len: usize, index: isize) -> ! {
                    panic!("Tried to index slice of length {len} with index {index}, which is too negative to index backwards here.");
                }

                out_of_bounds(self.len(), index);
            },
        }
    }
}
1
2
3
4
5
6
7
use signed_indexing::arrays::prelude::*;

let array = [1, 2, 3];

// Unambiguous:
let first = array[0];
let last = array[-1];

This is likely a rather niche use-case.

It could also be useful in the context of scoped-fallback-implementations.

Implicit import of supertrait implementations of scoped implementations defined on discrete types

As subtype implementations defined on discrete types always require specific supertrait implementations, the import of these supertrait implementations could be made implicit.

This would also affect implementation environments modified in generic arguments, changing

1
let min_heap: BinaryHeap<u32 as PartialOrd in reverse + Ord in reverse> = [1, 3, 2, 4].into();

to

1
let min_heap: BinaryHeap<u32: Ord in reverse> = [1, 3, 2, 4].into();

and

1
dbg!(<u32 as Ord in reverse where u32 as PartialOrd in reverse>::cmp(&1, &2)); // […] = Greater

to

1
dbg!(<u32 as Ord in reverse>::cmp(&1, &2)); // […] = Greater

The downside is that use-declarations would become less obvious. Implied supertrait implementation imports could be enabled only for implementation environments specified inline on generic type parameters as e.g. Type as Ord in module to avoid this.

If this is added later than scoped impl Trait for Type, then private scoped implementations must not be implicitly exported through this mechanism. (It’s likely a good idea to not allow that anyway, as it would be surprising.) Making previously crate-private implementations available that way could lead to unsoundness.

Alternatively

It could be enough to allow inferring the module explicitly by writing _ instead of its SimplePath, so that the snippets above become

1
let min_heap: BinaryHeap<u32 as PartialOrd in _ + Ord in reverse> = [1, 3, 2, 4].into();

and

1
dbg!(<(u32 as PartialOrd in _) as Ord in reverse>::cmp(&1, &2)); // […] = Greater

Here, too, the inference should only be of required supertrait implementations based on explicitly chosen implementations of their subtraits.

Conversions where a generic only cares about specific bounds’ consistency

With specialisation and more expressive bounds, an identity conversion like the following could be implemented:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// In the standard library.

use std::mem;

impl<T, U, S: BuildHasher> From<HashSet<T, S>> for HashSet<U, S>
where
    T: ?Hash + ?Eq, // Observe implementations without requiring them.
    U: ?Hash + ?Eq,
    T == U, // Comparison in terms of innate type identity and observed implementations.
{
    fn from(value: HashSet<T, S>) -> Self {
        unsafe {
            // SAFETY: This type requires only the `Hash` and `Eq` implementations to
            //         be consistent for correct function. All other implementations on
            //         generic type parameters may be exchanged freely.
            //         For the nested types this is an identity-transform, as guaranteed
            //         by `T == U` and the shared `S` which means the container is also
            //         guaranteed to be layout compatible.
            mem::transmute(value)
        }
    }
}

This could also enable adjusted borrowing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// In the standard library.

use std::mem;

impl<T, S: BuildHasher> HashSet<T, S> {
    fn as_with_item_impl<U>(&self) -> HashSet<U, S>
    where
        T: ?Hash + ?Eq, // Observe implementations without requiring them.
        U: ?Hash + ?Eq,
        T == U, // Comparison in terms of innate type identity and observed implementations.
    {
        unsafe {
            // SAFETY: This type requires only the `Hash` and `Eq` implementations to
            //         be consistent for correct function. All other implementations on
            //         generic type parameters may be exchanged freely.
            //         For the nested types this is an identity-transform, as guaranteed
            //         by `T == U` and the shared `S` which means the container is also
            //         guaranteed to be layout compatible.
            &*(self as *const HashSet<T, S> as *const HashSet<U, S>)
        }
    }
}

(But at that point, it may be better to use something like an unsafe marker trait or unsafe trait with default implementations.)

Sealed trait bounds

This is probably pretty strange, and may not be useful at all, but it likely doesn’t hurt to mention this.

Consider ImplEnvironment clauses in bounds like here:

1
2
3
4
5
6
use another_crate::{Trait, Type1, Type2};

pub fn function<T as Trait in self>() {}

pub use impl Trait for Type1 {}
pub use impl Trait for Type2 {}

With this construct, function could privately rely on implementation details of Trait on Type1 and Type2 without defining a new sealed wrapper trait. It also becomes possible to easily define multiple sealed sets of implementations this way, by defining modules that export them.

Overall this would act as a more-flexible but also more-explicit counterpart to sealed traits.

Iff the caller is allowed to use this function without restating the binding, then removing the scope would be a breaking change (as it is already with bindings captured on type parameters in public signatures, so that would be consistent for this syntactical shape).

That convenience (automatically using the correct implementations even if not in scope) also really should exist only iff there already is robust, near-effortless tooling for importing existing scoped implementations where missing. Otherwise this feature here would get (ab)used for convenience, which would almost certainly lead to painful overly sealed APIs.

Binding an implementation in a call as function::<T as Trait in a>() while it is constrained as fn function<T as Trait in b>() { … } MUST fail for distinct modules a and b even if the implementations are identical, as otherwise this would leak the implementation identity into the set of breaking changes.

Glue crate suggestions

If crates move some of their overlay features into glue crates, as explained in unblock-ecosystem-evolution, it would be nice if they could suggest them if both they and e.g. Serde were cargo added as direct dependencies of a crate currently being worked on.

An example of what this could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[package]
name = "my-crate"
version = "0.1.2"
edition = "2021"

[dependencies]
# none

[suggest-with.serde."1"]
my-crate_serde_glue = "0.1.0"

[suggest-with.bevy_reflect."0.11"]
my-crate_bevy_reflect_glue = "0.1.2"

[suggest-with.bevy_reflect."0.12"]
my-crate_bevy_reflect_glue = "0.2.1"

(This sketch doesn’t take additional registries into account.)

Ideally, crates.io should only accept existing crates here (but with non-existing version numbers) and Cargo should by default validate compatibility where possible during cargo publish.

Reusable limited-access APIs

Given a newtype of an unsized type, like

1
2
#[repr(transparent)]
pub struct MyStr(str);

for example, there is currently no safe-Rust way to convert between &str and &MyStr or Box<MyStr> and Box<str>, even though in the current module which can see the field this is guaranteed to be a sound operation.

One good reason for this is that there is no way to represent this relationship with a marker trait, since any global implementation of such a trait would give outside code to this conversion too.

With scoped impl Trait for Type, the code above could safely imply a marker implementation like the following in the same scope:

1
2
3
4
5
// Visibility matches newtype or single field, whichever is more narrow.

use unsafe impl Transparent<str> for MyStr {}
use unsafe impl Transparent<MyStr> for str {}
// Could symmetry be implied instead?

(Transparent can and should be globally reflexive.)

This would allow safe APIs with unlimited visibility like

1
2
3
4
5
6
pub fn cast<T: Transparent<U>, U>(value: T) -> U {
    unsafe {
        // SAFETY: This operation is guaranteed-safe by `Transparent`.
        std::mem::transmute(value)
    }
}

and

1
2
3
unsafe impl<T: Transparent<U>, U> Transparent<Box<U>> for Box<T> {}
unsafe impl<'a, T: Transparent<U>, U> Transparent<&'a U> for &'a T {}
unsafe impl<'a, T: Transparent<U>, U> Transparent<&'a mut U> for &'a mut T {}

which due to their bound would only be usable where the respective T: Transparent<U>-implementation is in scope, that is: where by-value unwrapping-and-then-wrapping would be a safe operation (for Sized types in that position).

Overall, this would make unsized newtypes useful without unsafe, by providing a compiler-validated alternative to common reinterpret-casts in their implementation. The same likely also applies to certain optimisations for Sized that can’t be done automatically for unwrap-then-wrap conversions as soon as a custom Allocator with possible side-effects is involved.

If a module wants to publish this marker globally, it can do so with a separate global implementation of the trait, which won’t cause breakage. (As noted in efficient-compilation, the compiler should treat implementations of empty traits as identical early on, so that no code generation is unnecessarily duplicated.)

Could sharing pointers like Arc inherit this marker from their contents like Box could? I’m unsure. They probably shouldn’t since doing this to exposed shared pointers could easily lead to hard-to-debug problems depending on drop order.

A global

1
unsafe impl<T: Transparent<U>, U> Transparent<UnsafeCell<U>> for UnsafeCell<T> {}

should be unproblematic, but a global

1
unsafe impl<T> Transparent<T> for UnsafeCell<T> {}

(or vice versa) must not exist to allow the likely more useful implementations on &-like types.

This post is not licensed for any purpose, unless otherwise noted.
It is provided AS IS without any guarantee of correctness beyond those required for legal reasons.