Search code examples
rustabstract

How to work around the lack of abstract classes in rust?


Let's say I have common logic that depends intimately on data members as well as a piece of abstract logic. How can I write this in rust types without rewriting the same code for each implementation?

Here's a toy example of what I might write in scala. Note that the abstract class has concrete logic that depends on both the data member name and abstract logic formatDate().

abstract class Greeting(name: String) {
  def greet(): Unit = {
    println(s"Hello $name\nToday is ${formatDate()}.")
  }

  def formatDate(): String
}

class UsaGreeting(name: String) extends Greeting {
  override def formatDate(): String = {
    // somehow get year, month, day
    s"$month/$day/$year"
  }
}

class UkGreeting(name: String) extends Greeting {
  override def formatDate(): String = {
    // somehow get year, month, day
    s"$day/$month/$year"
  }
}

This is just a toy example, but my real life constraints are:

  • I have several data members - not just one (name).
  • Every subclass has the same complex methods that depends on both those data members and abstract functions specific to the subclass.
  • For good API design, it's important the implementing structs continue to hold all those data members and complex methods.

Here are some somewhat unsatisfactory ideas I had that could make this work in rust:

  • I could require a get_name() method on the trait that every implementation would need. But this seems unnecessarily verbose and might also cause a performance hit if the getter doesn't get inlined.
  • I could avoid using a rust trait altogether, instead making a struct with an additional data member that implements the missing abstract logic. But this makes the abstract logic unavailable at compile time, and would definitely cause a performance hit.
  • I could again avoid using a rust trait altogether, instead making a struct with a generic whose associated functions complete the abstract logic. So far this is my best idea, but it feels wrong to use generics to fill in missing logic.

I'm not fully happy with these ideas, so is there a better way in rust to mix abstract logic with concrete logic that depends on data members?


Solution

  • The most general solution seems to be my original 3rd bullet: instead of a trait, make a struct with a generic whose associated functions complete the functionality.

    For the original toy Greeting example Denys's answer is probably best. But a more general solution that addresses main question is:

    trait Locale {
      pub fn format_date() -> String;
    }
    
    pub struct Greeting<LOCALE: Locale> {
      name: String,
      locale: PhantomData<LOCALE>, // needed to satisfy compiler
    }
    
    impl<LOCALE: Locale> Greeting<LOCALE> {
      pub fn new(name: String) {
        Self {
          name,
          locale: PhantomData,
        }
      }
    
      pub fn greet() {
        format!("Hello {}\nToday is {}", self.name, LOCALE::format_date());
      }
    }
    
    pub struct UsaLocale;
    
    impl Locale for UsaLocale {
      pub fn format_date() -> {
        // somehow get year, month, day
        format!("{}/{}/{}", month, day, year)
      };
    }
    
    pub type UsaGreeting = Greeting<UsaLocale>;
    pub type UkGreeting = ...