'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 1:[1]

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 Greeting example Denys's answer is probably best. But if Greeting also needs to be generic with a type depending on the implementation, it no longer works. In that case, this would be the most general solution, where T is an implementation-specific type.

This pattern of adding an extra generic should be able to implement any shared logic that depends on both data members and abstract logic (effectively an abstract class):

trait Locale<T> {
  pub fn local_greeting(info: T) -> String;
}

pub struct Greeting<T, LOCALE> where LOCALE: Locale<T> {
  name: String,
  locale_specific_info: T,
  locale: PhantomData<LOCALE>, // needed to satisfy compiler
}

impl<T, LOCALE> Greeting<T, LOCALE> where LOCALE: Locale<T> {
  pub fn new(name: String, locale_specific_info: T) {
    Self {
      name,
      locale_specific_info,
      locale: PhantomData,
    }
  }

  pub fn greet() {
    let local_greeting = LOCALE::local_greeting(self.locale_specific_info);
    format!("Hello {}\nToday is {}", self.name, local_greeting);
  }
}

pub struct UsaLocale {}
impl Locale<Date> for UsaLocale {
  pub fn local_greeting(info: Date) -> {
    format!("{}/{}/{}", info.month, info.day, info.year)
  };
}

pub type UsaGreeting = Greeting<Date, UsaLocale>;
...
pub type UkGreeting = ...

Solution 2:[2]

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)

Solution 3:[3]

You can add function that will return references to required fields and define default implementation for function. It is common technique in java or C# (get; set;)

trait Greeting: 
{
    fn get_name(&self) -> &str;
    fn format_date(&self) -> &str;
    fn greet(&self)
    {
        println!("Hello {}\n Today is {}", self.get_name(), self.format_date());
    }
}

struct USAGreeting{name: String }
struct UKGreeting{name: String }

impl Greeting for USAGreeting
{
    fn get_name(&self) -> &str { &self.name }

    fn format_date(&self) -> &str {
        return "$month/$day/$year"
    }
}

impl Greeting for UKGreeting
{
    fn get_name(&self) -> &str { &self.name }

    fn format_date(&self) -> &str {
        return "$day/$month/$year"
    }
}

fn main() {
    let uk = UKGreeting { name: "UK Greeting!".to_owned() };
    let usa = UKGreeting { name: "UK Greeting!".to_owned() };

    let dynamic: Vec<&dyn Greeting> = vec![&uk, &usa];

    for greeter in dynamic
    {
        greeter.get_name();
        greeter.greet();
    }
}

Now Greeting is usable in dynamic or generic context. Ofc you have to define get_name() -> &str every time you implement Greeting. So workaround could look like this:

trait Named
{
    fn get_name(&self) -> &str;
}

trait Greeting: Named 
{
    fn format_date(&self) -> &str;
    fn greet(&self)
    {
        println!("Hello {}\n Today is {}", self.get_name(), self.format_date());
    }
}

impl Greeting for USAGreeting
{
    fn format_date(&self) -> &str {
        return "$month/$day/$year"
    }
}

impl Greeting for UKGreeting
{
    fn format_date(&self) -> &str {
        return "$day/$month/$year"
    }
}

It does not solve the problem but seperates it form Greeting trait. now you can use procedural macros (#[derive()]) to generate get_name automaticly.

For example procedural macro could look like this

use proc_macro::{self, TokenStream};
use quote::quote;
use syn;

#[proc_macro_derive(NamedMacro)]
pub fn describe(tokens: TokenStream) -> TokenStream {
    let ast: syn::DeriveInput = syn::parse(tokens).unwrap();

    let name = &ast.ident;

    quote! {
        impl Named for #name {
            fn get_name(&self) -> &str {
               &self.name
            }
        }
    }.into()

}

And then adding get_name() to struct is easy:

use my_macro::NamedMacro;

#[derive(NamedMacro)]
struct USAGreeting{ name: String }
#[derive(NamedMacro)]
struct UKGreeting{ name: String }

You can read about macros here

Procedural Macros

Macros

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3