Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

abstract

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?

like image 886
mwlon Avatar asked Mar 02 '23 13:03

mwlon


2 Answers

As you noticed, Rust isn't built around a class taxonomy principle, so the design is usually different and you should not try to mock OO languages in Rust.

You ask a very general question but there are a lot of specific cases calling for different solutions.

Very often when you're tempted in a OO language to define what objects are with classes, you'd use traits to specify some aspects of the behaviors of structs in Rust.

In your specific case, assuming the right solution shouldn't involve parameterization or a i18n utility, I'd probably use both composition and an enum for the way to greet:

pub struct Greeting {
    name: String,
    greeter: Greeter;
}
impl Greeting {
    pub fn greet(&self) -> String {
        // use self.greeter.date_format() and self.name
    }
}

pub enum Greeter {
    USA,
    UK,
}
impl Greeter {
    fn date_format(&self) -> &'static str {
        match self {
           USA => ...,
           UK => ...,
        }
    }    
}

Your composite implementation just has to switch on the variant when needed.

(note that I don't write the implementation in this case because perf concerns would probably call in Rust for a different design and not a dynamically interpreted pattern, but that would bring us far from your question)

like image 184
Denys Séguret Avatar answered Mar 07 '23 10:03

Denys Séguret


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 = ...
like image 23
mwlon Avatar answered Mar 07 '23 12:03

mwlon