Pre-Pre-RFC: Using inherent impls to complete trait impls


#1

Often, I want to create a trait which contains a subset of the methods in the inherent impl of a particular type (possibly a type from a crate that isn’t mine). The most common reason to do this is mocking, but it also enables a certain amount of encapsulation by enumerating a subset of that type’s methods that will be used in this context. And of course I may actually have a new trait abstraction between two types that the original authors of the types (possibly me) didn’t realize was there.

Unfortunately, doing this requires some boilerplate:

  • First I have to define the trait.
  • Then I have to implement it for the type with the inherent methods, writing a call to the inherent method on each of them.

What if I only had to do the first one? What if, when presented with an incomplete trait impl, rustc checked the inherent impl for the type being impl’d an item with the same name and type signature and used that method by default? Then, all I’d have to do is impl TcpStreamable for TcpStream { } to provide an impl.

I’m not sure how this would interact with the idea of partial impls from the specialization RFC. Also unresolved is how smart should this be: e.g. should impl<T: Clone> Foo for Vec<T> look at methods in the inherent impl of Vec<T> or only Vec<T: Clone>?


#2

I’ve considered the idea of declaring “inherent traits” (basically a trait impl whose methods are also considered inherent methods, effectively) as an alternative solution to this same problem.


#3

I’d love to see some solution to this problem. This kind of feature could go a long way toward simplifying the standard library for newcomers, by allowing us to easily provide a larger number of methods inherently (and shift conventions in that direction).

As you say, there’s some interaction with partial impls – and indeed, with default methods on traits. We’d probably have to provide some precedence rules. So this precise proposal would make it a bit harder to tell where an item was coming from.

Regarding bounds, it’s easy enough to check that the corresponding inherent method is available under whatever constraints the trait impl imposes.

As @nikomatsakis says, though, another option is to drive it from the trait impl side. That has the benefit of greater clarity as to where items are coming from – it just follows the usual rules for traits. Presumably, if you mark a trait impl as inherent, you’re not allowed to have any overlapping items in other inherent blocks. There also aren’t questions about differences in bounds (although there’s also less flexibility, since the trait bounds effectively become the inherent item bounds).

I haven’t been able to think of a great syntax for an inherent trait impl, though:

// normal trait impl
impl Write for File { 
    fn write(&mut self, buf: &[u8]) -> Result<usize> { ... }
}

// Possible ways to say "inherent":

// doesn't feel like it should just be an attribute
#[inherent]
impl Write for File { ... }

// `on` vs `for` -- probably too subtle
impl Write on File { ... }

// doesn't match the normal way of writing inherent impls
inherent impl Write for File { ... }

// requires repetition of Self
impl File, Write for File { ... }

#4

cc @sfackler


#5

How would inherent trait impls help with things like mocking someone else’s types?

I agree that precedence issues are a problem with this proposal though.


#6

Ah yes, that’s another downside of the approach that drives from trait impls – there has to be just one trait of interest. Whereas driving from the inherent impl means that you can have many trait impls pulling from it. Interesting!


#7

My intuition is that default methods and inherent methods are fairly unlikely to overlap, but maybe if this feature were implemented, people would write code differently. It seems to me that an inherent method should be preferred over the default method - I would consider it ‘more specific’ because its implemented for a concrete type whereas the Self type in a default method is a type variable (intuitively, it also has access to more information about the type - I think strictly more?).

Partial impls are more complicated. Perhaps there is a way to thread the inherent impl into the specialization ranking that isn’t particularly surprising. Here, for example, I would expect that the partial impl’s impl would be “more specific.”

trait Foo {
    fn foo(&self) { ... }
    fn bar(&self) { ... }
}

struct Bar<T> { ... }

// Less specific: all Bar<T>
impl<T> Bar<T> {
    fn bar(&self) { ... }
}

// More specific: only Bar<T: Clone>
partial impl<T: Clone> Foo for Bar<T> {
    fn bar(&self) { ... }
}

impl<T: Copy> Foo for Bar<T> {
    fn foo(&self) { ... }
}

I would expect whichever of the inherent or parital impl were more specific according to specialization rules to come first. If they were the same though, I don’t know what to do! I guess the partial impl should be preferred (because it also is scoped to the trait’s namespace)? Maybe lint as well?


#8

Thinking more, I think the rule should be:

  1. If an item is not defined in an impl, look at all the inherent impls and impls of this trait that match this type.

  2. Order the matching impls according to specialization rules, with the additional rule that a trait impl is more specialized than an inherent impl (but the default methods are inherently the least specialized).

  3. Moving through the list, the first item with a matching name and signature is used for this item.

  4. If no matching item is found, type error.

The only actual additions to specialization, then, are:

  • Inherent impls are added to the list of less specialized impls to check for items.
  • Inherent impls are less specialized than trait impls for the same type (but more specialized than a trait impl for a less specialized type).

As far as I can tell, this wouldn’t actually interact with other extensions to specialization like partial impls or the lattice algorithm.


#9

Given that inherent methods take precedence over trait methods, I wonder if a useful way to handle this impl would just be with a macro-like approach, similar to #[derive]. Basically, the idea would be that you can “auto-generate” the body of methods exactly of the form:

fn foo(&self, x: u32) {
    self.foo(x)
}

or perhaps, using fully qualified paths, something like:

fn foo(&self, x: u32) {
    Foo::foo(self, x)
}

(That formulation would also work for methods without self.)

I had hoped to suggest that we could just add #[derive(inherent)] on the impl or something to drive this, but that would not (I think) work, as it would have to consult the trait definition to find the set of arguments – and it still shares the confusion with specializable method definitions.

I feel somewhat uncomfortable with just looking at inherent methods without any kind of opt-in. The precise formulation that @withoutboats gave also seems backwards incompatible – it might change from using a definition found in a trait to using an inherent method, no? Moreover, I am nervous about trying to search for things with a “matching name and signature” – those kinds of queries are always challenging.


#10

To be clear, I mean that it could not be implemented as a simple syntax extension – it’d be have to be a rule that is more built-in to the compiler.


#11

You could add #[inherent(Foo)] on the trait, though, since all it needs from the type is the type’s name. This might be a good enough solution, honestly.

This is correct, unless we put inherent impls at the very end of specialization (or required you opt-in somehow) this would be a breaking change! :frowning: It seems extremely uncommon in practice, though. You would have to have a trait’s default method and a type’s inherent method with the same name and signature, but diverging behavior, and then not override the default method when implementing that trait for that type.

Obviously we don’t want to be too opinionated about good code (and “you’re doing it wrong” is not a compelling justification to someone whose code broke by a change), but I would call this decidedly bad practice. It already will result in lookups that are hard to reason about. For example. I feel optimistic that this might be a case where no existing code would break.


#12

You could, but I think it will often happen that you do not control the trait, so I’d prefer an annotation that gets placed on the type.


#13

In my primary use case for this (mocking) I control the trait and not the type. I’d prefer a solution that followed the orphan rules rather than always requiring you control the trait or always requiring you control the type.


#14

This is why I was annotating the impl :slight_smile:


#15

May I suggest that although they don’t have a strictly identical purpose, the proposed feature and this one could nicely be merged in a single one. I could imagine something like:

struct Type { … }

impl TraitA for Type {
    use expressionB as TraitB for method1, method2;
    use expressionC as TraitC for *;
}

Sure it is longer than a macro-like approach. But also far more powerful.