Writing Rust-y code: Implementation of trait with default functions

TL;DR: Is it possible to add functions to a trait, and then not have to re-implement all functions for the implementation? I assume I'm totally missing how to do it rust-y, so here is the longer explanation:

Longer version:
I'm trying to de-Python my brain and it's going okay. I'm doing a XML parser where I "hand off" the processing of certain parts of the document to different modules/classes. Sample XML:

<xml>
<car>
  <name>First car</name>
  <brand>Volvo</brand>
</car>
<plane>
  <name>First plane</name>
  <brand>Boeing</brand>
</plane>
<car>
  <name>Second car</name>
  <brand>Volvo</brand>
</car>
</xml>

In reality it's much more complex and about 500-1000MB in size.

Using quick-xml I then do:

            Ok(Event::Start(ref e)) => {
                match e.name() {
                    b"car" => car::Car::process(&mut reader, event);
                    b"plane" => plane::Plane::process(&mut reader, event);
                    _ => return Err("Unexpected Tag"),
                }
            },

Now this works, and Car for example is a struct with an implementation. However, in reality Car and Plane is actually pretty similar so I want to re-use the core "xml processing logic" between them, similar to having a base class in Python.

My first attempt was:

trait Vehicle {
   fn process(reader: &mut Reader<BufReader<File>>, event: &quick_xml::events::BytesStart) {
     ...
     self.handle_something(...) // this obviously doesn't work
     ....
   }
}

struct Car {
  name: String,
  brand: String,
}

impl Vehicle for Car {  // won't compile since I'm not implementing the process function
    fn handle_something() {
       
    }
}

So I'm trying to keep the base function (process) the same in both Car and Plane, and have that base function hand over to a function (handle_something) which I do override.

In Python I would do:

class Vehicle() {
   def process() {
      ...
      self.handle_something()
      ..
   }
}

class Car(Vehicle) {
    def handle_something() {
        ...
   }
}

And it would inherit the process function from Vehicle

Obviously I'm missing something, but despite spending a few hours on this (and reading the rust-lang book - which is awesome! and stack overflow and blog posts) I'm not wrapping my head around how I would do this.

Any helpers would be greatly appreciated :slight_smile:

You have to give the compiler a guarentee that all implementations of Vehicle will definitely have a self.handle_something() method to call, otherwise your default method implementations can't use it.

The easiest way to do this would be adding the method to the trait, and making it mandatory to implement:

trait Vehicle {
    fn handle_something();

    fn process(reader: &mut Reader<BufReader<File>>, event: &quick_xml::events::BytesStart) {
        self.handle_something();
    }
}

struct Car {
    name: String,
    brand: String,
}

impl Vehicle for Car {
    // the default implementation of `process` is used,
    // but you still have to implement `handle_something`:

    fn handle_something() {
       
    }
}

The previous post is probably the easiest way forward.

This is a good opportunity to stop and "de-Python" a bit. If something doesn't need to change behaviour, it doesn't need to be on a trait. So you could have the part you do want to customize be in the trait (Vehicle::handle_something), but just have a function like

fn process_vehicle<V: Vehicle>(reader: &mut Reader<BufReader<File>>, event: &quick_xml::events::BytesStart) {
    ...
    ... call V::handle_something() ...
    ...
}

Whether that's better then having it on the trait with a default implementation I'll leave up to you to decide. It probably depends, to some extent, on who's implementing the traits and what guarantees you need. When it's a function instead of a trait method it's impossible to override it for different types, with both the good and bad consequences of that reduced flexibility.

1 Like

Thank you everyone, you got me in the right direction. Turns out my issue wasn't with actual trait and implementation which I thought, but an error of scope. I wasn't able to call car because it was in another module and I hadn't imported the trait, doh!

mod test {
    pub trait Vehicle {
        fn handle_something() -> u32;
    
        fn process() -> u32 {
            1
        }
    }
    
    pub struct Car {
        name: String,
        brand: String,
    }
    
    impl Vehicle for Car {
        // the default implementation of `process` is used,
        // but you still have to implement `handle_something`:
    
        fn handle_something() -> u32{
           1
        }
    }
}

use test::Vehicle; // This one was missing

fn main() {
    test::Car::process();
}

And that gave me an error function or associated item not found in Car`` which I miss-read as didn't exist not realizing that it didn't exist because of me not importing it.

Thank you all :slight_smile: