Idea: partial impls

Motivation

Currently, it’s impossible to restrict a default method body to a set of bounds more specific to those of the trait. (Prior discussion: Conditional default trait method impls). In addition, there are cases where, for correctness, implementing one trait places restrictions on implementations of other traits, but enforcing those restrictions is difficult at present. Finally, it is currently impossible to define trait methods with default bodies that are impossible to override.

Explanation

We introduce a new type of trait impl: partial impl. The syntax is exactly the same as a normal trait impl, except that:

  • The contextual keyword partial is added before the keyword trait, optionally preceded with default, and
  • Not all the trait’s items need to be included, any or all may be omitted.

A partial impl Trait for Type does not actually cause Trait to be implemented; a non-partial impl is necessary for this. Such an impl is necessary to access any of the items defined in the partial impl block. But that non-partial impl can omit any items that were already defined by the applicable partial impl. In addition, if the partial impl was not annotated with default, then the non-partial impl is not permitted to override its items.

partial impls must obey all the same orphan rules as non-partial impls. In addition, non-default partial impls are not permitted to overlap if they both define the same item. default partial impl overlaps are permitted, however; the rules for them are defined later in this document.

Examples

//! Providing defaults that are not always applicable.
//! Example from https://internals.rust-lang.org/t/conditional-default-trait-method-impls/15412

fn require_sized<T: Sized>(_: &T) {}

trait Foo {
    fn foo(&self);
}

default partial impl<T: Sized> Foo for T {
    fn foo(&self) {
        require_sized(self);
    }
}

struct Bar;
impl Foo for Bar {
    // no need to provide `foo()`, it’s taken from the `partial impl`.
    // But I could override it if I wanted, as the `partial impl` is marked `default`.
}

fn main() {
    let x: &dyn Foo = &Bar;
    x.foo();
}
//! Enforcing relationships between traits.

trait Foo {
    fn frob(&self);
    fn bork(&self);
}

/// Implementors of `BorkFrobsTwice` implement `Foo` such that
/// `Foo::bork()` calls `Foo::frob()` exactly twice. This can be relied upon for soundness.
trait BorkFrobsTwice: Foo {}

partial impl Foo for BorkFrobsTwice {
    fn bork(&self) {
        self.frob();
        self.frob();
    }
}
//! Non-overridable auxiliary trait methods.

trait Foo {
    fn frobnicate(&self);

    /// Calls `frobnicate` twice. Does nothing else.
    /// You can rely on that for soundness.
    fn frobnicate_twice(&self);
}


partial impl<T: ?Sized> Foo for T {
    fn frobnicate_twice(&self) {
        self.frobnicate();
        self.frobnicate();
    }
}

Precedence of defaults

When a non-partial trait impl does not define one of the trait’s items, and the item is not provided in an applicable non-default partial impl either, we try to take the definition from an applicable default partial impl, or from the default implementation provided in the trait definition. However, there may be multiple defaults that could apply, so we choose among them with the following rules:

When looking for a default implementation of an item to complete an impl<...> Trait<...> for Type<...> block (where ... represents 0 or more generics):

  1. First, look for implementations in applicable default partial impl<...> Trait<...> for Type<...> blocks.
    • “Applicable” means that the where clauses can’t be more restrictive than those of the impl block being completed.
    • If there are several, choose the one with the most restrictive bounds.
      • This includes bounds on Type, as well as bounds on the generic parameters of Trait.
    • If that is not enough to disambiguate, choose the one in the most “downstream” crate—the one that depends on the other.
    • If that is still not enough to disambiguate, then no default applies, and the impl being completed must provide the item itself.
  2. If none was found, look for implementations in applicable default partial impl<T: Bound> Trait for T blocks.
    • If there are several, choose the one with the most restrictive bounds.
    • If that is still not enough to disambiguate, then no default applies, and the impl being completed must provide the item itself.
  3. If none was found, look for a default in the trait definition.
    • If there is none, then the impl being completed must provide the item itself.
1 Like

Isn't this similar to specialization?

Though there is some slight overlap in use-cases, this is not specialization, and does not have the soundness issues of specialization.

In fact, I’ve edited the OP to change the syntax slightly, in order to remove the conflict with specialization.

What's the benefit over splitting frombnicate_twice() into a separate trait?

trait Frobnicate {
    fn frobnicate(&self);
}

trait FrobnicateTwice {
    fn frobnicate_twice(&self);
}

impl<T> FrobnicateTwice for T
where
    T: Frobnicate,
{
    fn frobnicate_twice(&self) {
        self.frobnicate();
        self.frobnicate();
    }
}

You don’t have to import FrobnicateTwice separately.

Though trait aliases would solve that.


A less powerful variant that’s strictly “like existing default method impls but they can be constrained separately from the method itself”:

default impl Trait // no `for Type` here
where 
    Self: …
{
    …
}

You couldn’t have default impls for concrete types (or type constructors), but is that a big loss?

2 Likes