Priorities for trait implementations

Disclaimer: This seems to have been suggested back in 2014, but back then people were suggesting using negative trait bounds as an alternative. As those aren't really something we want to make widely available, I think we need another solution.

The problem: It feels to me that traits aren't really done yet. They promote a "design by contract" approach, but while it's possible to say "if I can do Foo AND Bar, then I can do Baz," it's not really possible right now to say "if I can do Foo OR Bar, then I can do Baz." This severely limits the scope of what you can do with traits.

The problem with the natural approach (implementing a trait multiple times with different trait bounds) is that currently the compiler doesn't know what to do in the case where both sets of bounds are met. For instance:

// What do you compile in the case where `T: Foo + Bar`?
impl<T: Foo> SomeTrait for T {}
impl<T: Bar> SomeTrait for T {}

Proposed solution: Specialisation seems really useful, but only really gets us part of the way there. The proposed solution is to instead manually decide which impl block gets priority, like this:

trait SomeTrait {
    fn do_something(self);
}

trait Foo {
    fn foo(self);
}

trait Bar {
    fn bar(self);
}

#[priority(0)]
impl<T: Foo> SomeTrait for T {
    fn do_something(self) {
        self.foo()
    }
}

#[priority(1)]
impl<T: Bar> SomeTrait for T {
    fn do_something(self) {
        self.bar()
    }
}

If T:Foo, then do_something() will call foo(). If T:Bar, then do_something() will call bar(), but most importantly, if T:Foo+Bar, then do_something() will call foo(), as we've manually specified which version to compile when both impl blocks are valid.

What's more, this system could be extended to other areas where trait bounds might conflict with each-other. For instance, you might want to have different implementations of the same function to relax the bounds on its parameters. A #[priority] attribute would allow you to do that.

Overall, I think that having something like this in the language would allow for much greater power and control when using traits and generics, and could be very positive for the ecosystem as a whole. Given that negative trait bounds are a no-go, how would people feel about having this feature in the language?

Since specialization is a special-case of this kind of feature, it would have (to somehow deal with) the same soundness problems as specialization.

8 Likes

I'm not really a fan of priority numbers. It always reminds me too much of !important. And there's no good way to make them work across crates, as it just leads to loudness priority wars.

Note that marker_trait_attr - The Rust Unstable Book is in the works for the simple OR cases, though only ones that don't have conflict issues because it doesn't allow associated things.

Note that negative bounds can work, if what they look for is an explicit negative impl, rather than just the absence of an impl.

So to match a where T: !Copy one would need an impl !Copy for Foo {}. And negative impls like that exist on nightly with negative_impls - The Rust Unstable Book .

11 Likes

There is a fundamental issue that any solution to this problem will meet, this is the infamous specialization unsoundness. The problem is that you can have lifetime bounds on traits. This means that you may have a single type that only partially implements a trait depending on its lifetime. As a result, a single type can have two implementations for a single trait. Suppose:

impl<'a> Bar for &'a str {
  fn bar(self) {}
}
impl Foo for &'static str {
  fn foo(self) {}
}
// now `&'static str` uses `Foo`
// but `&'a str` uses Bar

In this case &str has two different implementations for SomeTrait because of lifetime bounds. In my opinion, the only safe way to resolve this is to downcast the &'static str to a &'a str, since 'static: 'a. However, I don't know if more complex cases exist where this kind of resolution is impossible.

Concerning your proposal, it would be very counter intuitive if &str resolved to the Bar implementation, even though Foo had precedence. All in all, I prefer a more restrictive form of negative trait bounds to address this issue (although it also suffers from this same unsoundness).

2 Likes

I believe that doesn't work when there are multiple lifetime parameters with outlives bounds between them.

A bit off topic, but what if we do resolution stage of specialization on lifetime erased version of our impl set?

That’s roughly what was proposed here way back in 2018:

I think it’s a workable approach, quite possibly the only workable approach, but I haven’t heard anything about it since then.

1 Like

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