2

I haven't wandered the path of advanced traits much, but I'm wondering if it's possible to save re-writing / copying and pasting nine functions by creating a trait which overrides just one or three functions of a more complex trait.

This was some experimenting I did this evening with serde_json's PrettyFormatter where I'd like to create a version of the PrettyFormatter that just changes how a Vec is printed.

I should note that the idea came from this answer which differs in that I'm consuming serde_json and interested in removing code duplication but likely the answer is still "not possible, check the RFC". It seems wasteful to not be able to re-use code already available.

Here's my minimum case that seems to fail:

trait Formatter {
    fn begin_array_value(&self) {
        println!("Formatter called");
    }
    
    fn two(&self) {
        println!("two")
    }
    
    // ... pretend there are a few more functions ...
    
    fn ten(&self) {
        println!("ten")
    }

}

trait PrettyFormatter: Formatter {
    fn begin_array_value(&self) {
        println!("I'm pretty!");
    }
}

struct MyFormatter { }

// This fails:
impl PrettyFormatter for MyFormatter { }
// This works:
//impl Formatter for MyFormatter { }

fn main() {
    let formatter = MyFormatter { };
    formatter.begin_array_value();
}

Specifically, the error is this:

Standard Error

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `MyFormatter: Formatter` is not satisfied
  --> src/main.rs:16:6
   |
8  | trait PrettyFormatter: Formatter {
   |                        --------- required by this bound in `PrettyFormatter`
...
16 | impl PrettyFormatter for MyFormatter { }
   |      ^^^^^^^^^^^^^^^ the trait `Formatter` is not implemented for `MyFormatter`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

I can copy and paste ~320 the lines, but I'm a big fan of writing as little code as possible. If this is somehow possible, I'd want to submit a PR to that crate so others could just work from a PrettyFormatter trait.

RandomInsano
  • 1,204
  • 2
  • 16
  • 36
  • 1
    It's worth noting that the `PrettyFormatter` you linked in your edit is a struct, whereas the one in your example code snippet is a trait. – Silvio Mayolo Feb 07 '21 at 06:27

2 Answers2

2

No, traits can not override other traits' implementations.

The syntax trait PrettyFormatter: Formatter { ... } does not imply a inheritance-like relationship. Anything after the : is a constraint, a requirement for a concrete type to meet in order to implement it. Here it means that anything wanting to implement a PrettyFormatter must also implement Formatter. With that in mind, PrettyFormatter::begin_array_value has no relationship to Formatter::begin_array_value.

You can implement both traits for your MyFormatter struct:

impl Formatter for MyFormatter { }
impl PrettyFormatter for MyFormatter { }

But trying to call formatter.begin_array_value() will be met with the error indicating the call is ambiguous:

error[E0034]: multiple applicable items in scope
  --> src/main.rs:33:15
   |
33 |     formatter.begin_array_value();
   |               ^^^^^^^^^^^^^^^^^ multiple `begin_array_value` found
   |
note: candidate #1 is defined in an impl of the trait `Formatter` for the type `MyFormatter`
  --> src/main.rs:2:5
   |
2  |     fn begin_array_value(&self) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `PrettyFormatter` for the type `MyFormatter`
  --> src/main.rs:19:5
   |
19 |     fn begin_array_value(&self) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #1
   |
33 |     Formatter::begin_array_value(&formatter);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #2
   |
33 |     PrettyFormatter::begin_array_value(&formatter);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Is there a way to avoid re-implementing the nine other functions for PrettyFormatter?

You will have to implement them, but you can defer to the Formatter implementation like so:

trait PrettyFormatter: Formatter {
    fn begin_array_value(&self) {
        Formatter::begin_array_value(self);
    }

    fn two(&self) {
        Formatter::two(self);
    }

    // ... others
    
    fn ten(&self) {
        Formatter::ten(self);
    }
}

The ambiguous function call problem will still exist, but only if both traits are in scope. If the original Formatter isn't in scope, then there won't be any issue.

See the answers here for more info and other solutions.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
1

You're approaching this with a very C# mindset, and that's going to cause problems. Traits are not interfaces, although they occasionally act like them. For one thing, trait PrettyFormatter: Formatter does not say "anybody who implements PrettyFormatter automatically implements Formatter". In fact, it says just the opposite: "the only types allowed to implement PrettyFormatter are those which already implement Formatter". So, if you want to implement PrettyFormatter for your type, you need to do both.

impl PrettyFormatter for MyFormatter { }
impl Formatter for MyFormatter { }

But this introduces our second problem. Namely, you don't override trait methods in Rust. It simply doesn't work that way. What you've done in your code sample is define two distinct, unrelated trait functions called begin_array_value (one in Formatter and one in PrettyFormatter), and if you try to call one of them, Rust will get confused since there's two functions with that name. Again, this isn't a flaw of the language; this is Rust eschewing the principles behind languages like C# and Java in favor of a different abstraction pattern.

Which brings us to your original point and how we should approach code reuse, and unfortunately I may not have enough information about your particular use case to answer it to your satisfaction. Your minimal example never uses self, so in this case I would argue that the functions in questions shouldn't even be trait functions but should instead be standalone functions.

Let me rephrase the question as I understand it, and I may be wrong on this. It sounds to me like you have some complicated trait Formatter with a ton of methods, and then we know that we can implement all of those methods using a simpler trait PrettyFormatter which only requires a handful of easier methods. If that's all correct, then I'd suggest a blanket implementation.

trait Formatter {
  // A zillion methods
}

trait PrettyFormatter { // N.B. No supertrait
  // A nice subset of a zillion methods
}

impl<T> Formatter for T where T : PrettyFormatter {
  // Here, implement all zillion methods for Formatter using
  // only the few from PrettyFormatter
}

Now, any type of your choosing can implement PrettyFormatter, and in doing so, it will automatically implement Formatter without you having to do any extra work.

I would also stress that you shouldn't use the same names for the trait functions between the two traits, as it's just going to make calling those functions much harder and will be terribly unintuitive to any users of your code.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • Thanks for the answer! Unfortunately, this isn't my trait but the one built into serde_json. Seems like it's not going to be possible. What makes it somewhat confusing is that traits can have default implementations at all – RandomInsano Feb 07 '21 at 06:22